# Copyright (c) The btclib developers
# Distributed under the MIT software license, see the accompanying
# LICENSE file or https://opensource.org/license/mit for the full text.
"""Pedersen commitment functions.
In a commitment scheme the committer:
* decides (or is given) a secret message v
* decides a random secret r
* *commits* to v by applying the public commitment
scheme algorithm and producing a commitment C=Commit(r,v)
* makes C public
Later, when he reveals r and v, the verifier *opens* the
commitment checking if indeed C=Commit(r,v).
Pedersen commitment uses a public group of large order n
in which the discrete logarithm is hard.
In the case of an elliptic curve group, the generator G is
supplemented with a second random generator H and
the commitment algorithm is Commit(r,v)=rG+vH.
It is crucial for H to be Nothing-Up-My-Sleeve (NUMS), i.e.
the discrete logarithm of H with respect to G must be unknown.
**The octets say residuosity, where a SEC prefix says parity.**
libsecp256k1-zkp writes a commitment as one octet and then x, and that
octet reports whether y is a quadratic residue:
`secp256k1_pedersen_commitment_save` writes
`9 ^ secp256k1_fe_is_square_var(&ge->y)` and
`secp256k1_generator_serialize` writes `11 ^` the same bit, both in
`src/modules/generator/main_impl.h`;
`secp256k1_rangeproof_serialize_point` writes `1 ^` it, in
`src/modules/rangeproof/rangeproof_impl.h`. That third tag is the base
`ecc.rangeproof` hashes a point under and lifts a ring commitment's x
against, and that module reaches the codec below for both rather than
carrying one of its own. Lifting such an octet by the parity the
`02`/`03` of `curves.point_from_octets` carries answers the same point
for some x and its negation for others, with nothing in the octets to
say which reading wrote them, so the two are not one function under two
names.
The codec below is secp256k1's and takes no `ec`: the tags and the
width of x are libsecp256k1-zkp's format, and that library is
secp256k1's. `generator_from_seed` takes none for a different reason:
the map it runs is written around sqrt(-3) and the curve's own b, so it
is a construction of this field rather than a format read in it.
`commit` takes an `ec` because rG+v*gen is a sum of points, which every
curve has, and takes the generator rather than deriving one because a
commitment is only as binding as the generator behind it and a caller
with several assets has one per asset.
"""
from functools import lru_cache
from hashlib import sha256
from btclib_ecc._utils import (
assert_type,
bytes_from_octets,
hex_string,
int_from_bits,
int_from_integer,
)
from btclib_ecc.alias import INF, HashF, Integer, Octets, Point
from btclib_ecc.curves import Curve, bytes_from_point, mult, secp256k1
from btclib_ecc.curves.curve import _add, _assert_in_subgroup, _assert_valid_ec
from btclib_ecc.curves.curve_group import HEX_THRESHOLD
from btclib_ecc.exceptions import (
BTClibEccRuntimeError,
BTClibEccValueError,
)
from btclib_ecc.number_theory import legendre_symbol_var, mod_inv, mod_sqrt_var
__all__ = [
"assert_as_valid",
"bytes_from_commitment",
"bytes_from_generator",
"commit",
"commitment_from_octets",
"generator_from_octets",
"generator_from_seed",
"second_generator",
"verify",
]
# (ec, hf) is the cache key: both change the answer, and both are
# hashable -- Curve.__hash__ exists precisely so that equal curves
# share cache entries (curve.py's _eq_key), and hf is compared by
# identity, the same conservative choice _libsecp256k1_serves
# makes for sha256. maxsize is a number, not None: ec is
# caller-supplied, and an unbounded cache on it would be a memory
# leak. The cached value is a Point, i.e. a tuple, so returning the
# same one to every caller is safe -- there is no mutable object to
# share by accident.
[docs]
@lru_cache(maxsize=128)
def second_generator(ec: Curve = secp256k1, hf: HashF = sha256) -> Point:
"""Second (with respect to G) Nothing-Up-My-Sleeve (NUMS) generator.
A commitment rG+vH is only binding if nobody knows log_G(H): a
committer who did could open the same commitment to any (r, v) of
their choosing. H is therefore not chosen but derived -- the hash of
G is read as a candidate x-coordinate, and the candidate is
incremented until it lands on the curve, and on a curve of cofactor
above 1 in the subgroup generated by G too -- so that computing a
discrete logarithm relating H to G is the only way to a value this
function could also have produced, and nobody has one.
The result is cached on (ec, hf): it is a constant for that pair,
recomputing it on every call cost 71% of a commitment (issue
btclib-org/btclib#287).
For (secp256k1, sha256), this function's own default pair, the
derived H equals the H hardcoded as
`secp256k1_generator_h` in libsecp256k1-zkp -- the H of Elements and
of Confidential Transactions.
`tests/ecc/pedersen_test.py::test_second_generator` pins that value
against a literal, and
`tests/ecc/pedersen_test.py::test_second_generator_matches_zkp`
against `btclib_secp256k1.zkp.generator.h()`, which is that library
answering with its own copy rather than a transcription of it. No
published constant exists to pin either against on another curve or
hash function.
idea:
https://crypto.stackexchange.com/questions/25581/second-generator-for-secp256k1-curve
source:
https://github.com/BlockstreamResearch/secp256k1-zkp/blob/master/src/modules/generator/main_impl.h
"""
# the generator is read off the curve before anything is done with
# it, so an ec of no curve type is turned down here; and the
# refusal is inside the cache rather than in front of it, such an
# ec being a key like any other -- hashable, so lru_cache would
# take it, and never stored, exceptions not being cached
_assert_valid_ec(ec)
G_bytes = bytes_from_point(ec.G, ec, compressed=False)
hash_ = hf()
hash_.update(G_bytes)
hash_digest = hash_.digest()
x_H = int_from_bits(hash_digest, ec.nlen) % ec.n
while True:
try:
H = x_H, ec.y_even_var(x_H)
# a no-op where the cofactor is 1, so H is unchanged there
_assert_in_subgroup(H, ec)
except BTClibEccValueError:
x_H += 1
x_H %= ec.p
else:
return H
# what a seed occupies: `secp256k1_generator_generate`'s own `key32`
_SEED_SIZE = 32
# the prefixes `secp256k1_generator_generate_internal` hashes the seed
# under, the trailing space of each included: the C declares each an
# octet longer than what it writes, so no terminator is hashed
_FIRST_GENERATION = b"1st generation: "
_SECOND_GENERATION = b"2nd generation: "
# sqrt(-3) and (sqrt(-3) - 1)/2, the two field constants the map below
# is written around. libsecp256k1-zkp writes them as literals, `negc`
# there being the negation of the first, where the field answers them:
# for a p of 3 mod 4 `mod_sqrt_var` returns the root that is itself a
# square, and `test_the_map_constants_are_derived` holds that root
# against the literal. `ecc.ellswift._constants` derives the same root
# the same way, SwiftEC being written around it too.
_SQRT_MINUS_3 = mod_sqrt_var(-3 % secp256k1.p, secp256k1.p)
_HALF_SQRT_MINUS_3_LESS_1 = (_SQRT_MINUS_3 - 1) * pow(2, -1, secp256k1.p) % secp256k1.p
# the exponent `secp256k1_fe_sqrt` raises to: a root of a wherever a has
# one, on a p of 3 mod 4, and a square itself, (p + 1) // 4 being even on
# secp256k1's p
_ROOT_EXPONENT = (secp256k1.p + 1) // 4
def _shallue_van_de_woestijne(t: int) -> Point:
"""Return the point of the curve that field element maps to.
`shallue_van_de_woestijne` in `src/modules/generator/main_impl.h`,
which is Fouque and Tibouchi's *Indifferentiable Hashing to
Barreto-Naehrig Curves*: for `w = c*t/(1 + b + t**2)` the three
candidates `d - t*w`, `-(d - t*w + 1)` and `1 + 1/w**2` are
x-coordinates of which at least one is the curve's, and the first
that is names the point. `c` is sqrt(-3) and `d` is `(c - 1)/2`.
The joint denominator `j = (1 + b + t**2) * (-3*t**2)` is what
spares the second inversion, and it vanishes at t = 0 alone --
`1 + b + t**2` cannot, -8 being no square modulo p. The C reads
`secp256k1_fe_inv`'s own answer of zero for an inverse of zero and
so lands on `(d, f(d))` there, which is what the zero below writes
out.
The y is the one that is a quadratic residue, negated where t is
odd. The paper turns it on the Jacobi symbol of t; the C uses the
parity instead and gives the reason, which holds here: nothing above
reads t except through t**2, so any criterion that turns with the
sign of t answers a point of the same pair, and the parity is
cheaper than the symbol.
What is walked here is a digest of the seed, which is a secret
(`generator_from_seed` says why). zkp forms all three roots and
selects among them with `secp256k1_fe_cmov`; this forms all three too, each
root an exponentiation by a fixed exponent and squared back to tell
whether it is one; the inverse is `number_theory.mod_inv`, timed on a
random factor rather than on `j`. So every seed costs the same
operations, whichever candidate names the point. That is the regular
tier and not constant time: the operations are Python integers, whose
cost follows their operands, which `SECURITY.md` says of the whole
Python arithmetic.
An integer outside 0..p-1 is no field element and is refused, which
is `secp256k1_fe_set_b32_limit`'s own refusal of the digest
`generator_from_seed` hands it.
"""
p = secp256k1.p
if not 0 <= t < p:
err_msg = "field element not in 0..p-1: "
err_msg += f"{hex_string(t)}" if t > HEX_THRESHOLD else f"{t}"
raise BTClibEccValueError(err_msg)
t_2 = t * t % p
wd = (t_2 + secp256k1._b + 1) % p
x3d = -3 * t_2 % p
j = wd * x3d % p
j_inv = mod_inv(j, p) if j else 0
x_1 = (_HALF_SQRT_MINUS_3_LESS_1 - _SQRT_MINUS_3 * t_2 % p * x3d % p * j_inv) % p
x_2 = -(x_1 + 1) % p
x_3 = (1 + pow(wd, 3, p) * j_inv) % p
# every candidate's root and its verdict, before any is chosen
candidates = []
for x in (x_1, x_2, x_3):
y_squared = secp256k1._y2(x)
y = pow(y_squared, _ROOT_EXPONENT, p)
candidates.append((x, y, y * y % p == y_squared))
# the cascade `secp256k1_fe_cmov` writes: the second candidate where
# the first is no x-coordinate, and the third where neither is, which
# the map guarantees is one -- so the third's verdict is not read
first, second, third = candidates
x, y, _ = first if first[2] else second if second[2] else third
return x, p - y if t % 2 else y
[docs]
def generator_from_seed(seed: Octets, blind: Integer | None = None) -> Point:
"""Return the generator that seed derives, blinded where asked.
`secp256k1_generator_generate_internal`: the seed is hashed under
two prefixes, each digest is read as a field element and mapped to
the curve by `_shallue_van_de_woestijne`, and the two points are
summed. `secp256k1_generator.h` publishes the result as distributed
uniformly over the curve, with no known discrete logarithm with
respect to G or to any other generator this answers -- which is what
a confidential transaction committing each of several assets under
its own generator asks for, `commit` and `ecc.rangeproof` taking the
generator they work at.
`blind` adds `blind*G` to that sum, and one function takes it where
the C publishes a call per case: `secp256k1_generator_generate_blinded`
at a blinding factor of zero is `secp256k1_generator_generate`, which
is what `test_generator_generate` asserts of each of its own vectors.
A factor at or past n is refused, `secp256k1_scalar_set_b32`'s
overflow being what refuses it there; zero is not, the C answering
`secp256k1_generator_generate`'s generator for it.
The seed and the blinding factor are secrets, as zkp treats them:
the generator is published to hide which asset an output carries,
and the seed is that asset's tag. blind*G is `curves.mult` and each
sum `curves.curve._add`, so on the bindings arm their duration
follows neither; the map is Python on either arm, and regular in the
seed rather than constant time, `_shallue_van_de_woestijne` says how.
Constant time in both is `btclib_secp256k1.zkp.generator.generate_blinded`,
where the bindings are built with the zkp extension.
`second_generator` is the other generator this module names, and the
two are unrelated constructions: that one increments the hash of G
until it lands on the curve, where this maps a digest onto it.
source:
https://github.com/BlockstreamResearch/secp256k1-zkp/blob/master/src/modules/generator/main_impl.h
"""
seed_bytes = bytes_from_octets(seed, _SEED_SIZE)
Q = _add(
_shallue_van_de_woestijne(
int.from_bytes(sha256(_FIRST_GENERATION + seed_bytes).digest(), "big")
),
_shallue_van_de_woestijne(
int.from_bytes(sha256(_SECOND_GENERATION + seed_bytes).digest(), "big")
),
secp256k1,
)
if blind is None:
return Q
blind_int = int_from_integer(blind)
if not 0 <= blind_int < secp256k1.n:
raise BTClibEccValueError("blinding factor not in 0..n-1")
return _add(Q, mult(blind_int, secp256k1.G, secp256k1), secp256k1)
[docs]
def commit(r: Integer, v: Integer, gen: Point, ec: Curve = secp256k1) -> Point:
"""Commit to v under blinding factor r, returning rG+v*gen.
`gen` is the generator the commitment is made under and the caller
names it, as `secp256k1_pedersen_commit` takes its own `gen`:
`second_generator` is the one this library derives for a curve and a
hash function, `generator_from_seed` the map that derives any other,
and nobody can open the commitment to a different (r, v) for as long
as log_G(gen) is unknown, which is what both derivations buy.
No hash function, where `second_generator` takes one: a hash is how
a generator is derived and says nothing about a sum of points.
r=0 mod n is refused: it commits with no blinding at all, Q is then v*gen, a
point anyone who guesses v can recompute. The check is on r alone and not on
its range, because the sum of two blinding factors is a blinding factor too
-- a Pedersen commitment is additively homomorphic -- and is routinely >=
ec.n (issue btclib-org/btclib#1250). It also subsumes the former separate
check for r and v both landing on INF: with r=0 mod n excluded, Q lands
there only if v is 0 mod n too, which is an ordinary commitment to a zero
value and not a blinding failure.
The check on r is here and nowhere else in the module: `assert_as_valid`
recomputes the commitment through this function, and `verify`
turns the `BTClibEccValueError` an invalid r raises into `False`.
`gen` is refused here, and by `verify` ahead of its try, when it is
off the curve, at INF, or on a curve of cofactor above 1 outside the
subgroup generated by G. At INF the commitment is rG for every v,
which opens to any value; outside the subgroup n*Q cycles with v,
which leaks v mod the cofactor.
"""
# ahead of the check below, which reads ec.n: `mult` asks the same
# of the same ec, and only after that read
_assert_valid_ec(ec)
_assert_valid_generator(gen, ec)
r_int = int_from_integer(r)
if r_int % ec.n == 0:
err_msg = "invalid (unblinded) commitment: r is 0 mod n"
raise BTClibEccValueError(err_msg)
return _commit(r_int, v, gen, ec)
def _assert_valid_generator(gen: Point, ec: Curve) -> None:
"""Refuse a generator no commitment may be made under.
One that is off the curve, at INF, or outside the subgroup generated
by G. This is the public side's check: `_commit` is the private twin
and does not repeat it.
"""
ec.require_on_curve(gen)
if gen == INF:
raise BTClibEccValueError("generator is the point at infinity")
_assert_in_subgroup(gen, ec)
def _commit(r: int, v: Integer, gen: Point, ec: Curve) -> Point:
"""Return rG+v*gen, an r of 0 mod n included.
Both scalars are secrets, so each is its own `mult` and the two
products are summed by `curves.curve._add`: `double_mult_var` is the
same point in one call, and its work follows its scalars on both
arms, one `secp256k1_ec_pubkey_tweak_mul` per term on the bindings
and an interleaved wNAF in Python. `commit` refuses an r of 0 mod n
ahead of this; `ecc.rangeproof.rewind` does not, putting what it
recovered back against the commitment as
`secp256k1_rangeproof_verify_impl` does, which rebuilds it from the
two and compares with nothing refused first.
"""
# `mult` reads a None point as the generator, so a gen of no type
# would commit under G: it is refused here instead
ec.require_on_curve(gen)
return _add(mult(r, ec.G, ec), mult(v, gen, ec), ec)
[docs]
def assert_as_valid(
r: Integer, v: Integer, commitment: Point, gen: Point, ec: Curve = secp256k1
) -> None:
"""Refuse a commitment that (r, v) does not open.
The commitment is recomputed and compared; verify is the boolean
answer.
The type of the commitment is checked and its *value* is not: a `None`
compares unequal to every point, so `verify` reported a commitment of no
type at all as one that does not open, where a pair of ints that is no
commitment is exactly what False is for (issue btclib-org/btclib#814).
`is_on_curve` is deliberately not asked -- that would refuse a wrong value
too.
"""
assert_type(commitment, tuple, "commitment")
if commitment != commit(r, v, gen, ec):
raise BTClibEccRuntimeError("commitment verification failed")
def _assert_structurally_valid_(r: Integer, v: Integer, gen: Point, ec: Curve) -> None:
"""Raise for an r or v that is no integer, or an unusable generator.
Ahead of the try that turns everything else -- an r that is 0 mod n,
an (r, v) that does not open the commitment, a generator it was not
made under -- into False (issue btclib-org/btclib#2170).
An `Integer` is an int, or the octets or the hex text of one, and
`int_from_integer` is what reads the last two; text that is no
number leaves no opening to compare, which is the same unanswerable
question `ecc.dsa` refuses for a key spelling that is no key. An
r of 0 mod n is not that: it is an integer, and one this scheme
refuses for what it is worth rather than for what it is spelled,
so it stays False with the rest of `commit`'s refusals.
The generator is asked here too: one off the curve, at INF or outside
the subgroup generated by G is no generator a commitment may be made
under, and it is refused (issue btclib-org/btclib-ecc#76). The
commitment enters as a native point and no parse of it runs:
`assert_as_valid` checks its type and deliberately not its value, a
pair of ints that is no commitment being exactly what False is for
(issue btclib-org/btclib#814).
"""
int_from_integer(r)
int_from_integer(v)
_assert_valid_ec(ec)
_assert_valid_generator(gen, ec)
[docs]
def verify(
r: Integer, v: Integer, commitment: Point, gen: Point, ec: Curve = secp256k1
) -> bool:
"""Open the commitment and return True if valid.
Raises where r or v is spelled in a way no integer is, or where gen
is off the curve, at INF or outside the subgroup generated by G;
answers False for an opening that is well formed and merely does not
open this commitment. See `_assert_structurally_valid_`.
"""
_assert_structurally_valid_(r, v, gen, ec)
# ValueError and BTClibEccRuntimeError: a well-formed opening that does
# not recompute the commitment is False, and so is an r of 0 mod n;
# a caller's own mistake in r, v or gen is refused above
# rather than excluded from the except
try:
assert_as_valid(r, v, commitment, gen, ec)
except (ValueError, BTClibEccRuntimeError):
return False
return True
# the leading octet of each of libsecp256k1-zkp's serializations, before
# the bit that reports the residuosity of y is xored into it. Every tag
# is odd, so bit 0 of the octet is that bit complemented: it is set
# exactly where y is not a square, which is what
# `secp256k1_pedersen_commitment_load` reads to decide whether to negate
# the point `secp256k1_ge_set_xquad` lifted, and what
# `secp256k1_rangeproof_verify_impl` reads for the same decision. An
# even tag would leave bit 0 the bit itself, and that negation would
# take the wrong half.
#
# 1 is the tag whose other bits are all zero, so a rangeproof's octet is
# the residuosity bit alone: `secp256k1_rangeproof_serialize_point`
# writes `data[0] = !secp256k1_fe_is_square_var(&point->y)`, and a proof
# carries that bit in a packed field rather than in front of an x, which
# is why `ecc.rangeproof` calls `_bytes_from_point` and `_point_from_x`
# and never `_point_from_octets`
_RANGEPROOF_TAG = 1
_COMMITMENT_TAG = 9
_GENERATOR_TAG = 11
def _bytes_from_point(Q: Point, tag: int) -> bytes:
"""Return `tag ^ is_square(y)` and then x, at whichever tag.
The tag is the whole of what the encodings do not share, so what
they all refuse is asked here rather than once each: a pair that is
no point of secp256k1, and the infinity point, which has no x to
write.
The Legendre symbol is what `secp256k1_fe_is_square_var` answers,
and each computes it as a gcd rather than as an exponentiation.
"""
secp256k1.require_on_curve(Q)
if Q[1] == 0: # infinity point in affine coordinates
raise BTClibEccValueError("no bytes representation for infinity point")
residue = legendre_symbol_var(Q[1], secp256k1.p) == 1
return bytes([tag ^ int(residue)]) + Q[0].to_bytes(secp256k1.p_size, "big")
def _point_from_x(x: int, not_square: bool) -> Point:
"""Return the point x names, with the y the residuosity bit asks for.
`secp256k1_ge_set_xquad` answers the y that is a square, and the
negation is the one `secp256k1_pedersen_commitment_load` makes on
bit 0 of a tagged octet and `secp256k1_rangeproof_verify_impl` on a
ring commitment's own sign bit. Both refusals are
`y_quadratic_residue_var`'s, which says which: an x at or past p,
read there through `secp256k1_fe_set_b32_limit`, and an x that is
the x-coordinate of no point, which is what `secp256k1_ge_set_xquad`
fails on -- `secp256k1_pedersen_commitment_parse` asking
`secp256k1_ge_x_on_curve_var` ahead of any lift instead.
"""
y = secp256k1.y_quadratic_residue_var(x)
return x, secp256k1.p - y if not_square else y
def _point_from_octets(octets: Octets, tag: int, name: str) -> Point:
"""Return the point those octets name, lifted by residuosity.
`secp256k1_pedersen_commitment_parse`'s own refusal, at whichever
tag: `(input[0] & 0xFE) != 8` is the leading octet outside the pair,
written here against the tag it is given. The lift, and what else
it turns down, are `_point_from_x`'s, on bit 0 of the prefix.
"""
octets = bytes_from_octets(octets, secp256k1.p_size + 1)
prefix = octets[0]
if prefix & 0xFE != tag ^ 1:
raise BTClibEccValueError(f"not a {name}: prefix 0x{prefix:02x}")
x = int.from_bytes(octets[1:], byteorder="big")
return _point_from_x(x, bool(prefix & 1))
[docs]
def bytes_from_commitment(Q: Point) -> bytes:
"""Return a commitment as libsecp256k1-zkp serializes one.
`secp256k1_pedersen_commitment_serialize` hands back what
`_save` stored: `08` where the y of the commitment is a quadratic
residue and `09` where it is not, then x. `curves.bytes_from_point`
writes the parity of that same y, so the two answer the same octets
only where the y that is a square is also the even one.
"""
return _bytes_from_point(Q, _COMMITMENT_TAG)
[docs]
def commitment_from_octets(octets: Octets) -> Point:
"""Return the commitment libsecp256k1-zkp's octets name.
`secp256k1_pedersen_commitment_parse`. An Elements output carries
these, and this is the lift a caller holding them makes before
calling anything that takes the commitment as a point --
`ecc.rangeproof.RangeProof.pubk_rings`, `assert_as_valid`, `verify`.
"""
return _point_from_octets(octets, _COMMITMENT_TAG, "Pedersen commitment")
[docs]
def bytes_from_generator(Q: Point) -> bytes:
"""Return a generator as libsecp256k1-zkp serializes one.
`secp256k1_generator_serialize`, which is `bytes_from_commitment`'s
octet at another tag: `0a` for a y that is a quadratic residue and
`0b` for one that is not. `second_generator` is the generator this
library computes; a blinded asset generator is what else this format
carries.
"""
return _bytes_from_point(Q, _GENERATOR_TAG)
[docs]
def generator_from_octets(octets: Octets) -> Point:
"""Return the generator libsecp256k1-zkp's octets name.
`secp256k1_generator_parse`, whose `(input[0] & 0xFE) != 10` refuses
a commitment's own leading octet -- which is what
`test_generator_fixed_vector` puts to it in
`src/modules/generator/tests_impl.h`. The x is the same octets
either way, so the tag is the whole of what keeps the two formats
apart.
"""
return _point_from_octets(octets, _GENERATOR_TAG, "generator")