# 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.
"""`secp256k1_generator.h`: generators, and Pedersen commitments.
A generator is a second base point, unrelated to `G` by any known
discrete logarithm, that a Pedersen commitment `x*G + v*gen` blinds a
value `v` under. `h()` is the static one the header itself fixes and
`generate`/`generate_blinded` are the ways to make another; `parse`
and `serialize` are the octet boundary for either, and
`pedersen_commitment_parse`/`pedersen_commitment_serialize` are the same
boundary for the 33-byte commitment `pedersen_commit` builds.
`ISS 608 <https://github.com/btclib-org/btclib-secp256k1/issues/608>`_'s
own body names a downstream consumer already pinned against the
x-coordinate of `secp256k1_generator_h`, independently of this library:
`h()` is what turns that pin into an assertion against the library
itself rather than against a copied literal.
Every function here needs `BTCLIB_LIBSECP256K1_ZKP`'s extension, and none
of them reaches for it at import time: `context._bindings()` is where
each defers to it, on the first call rather than on `import
btclib_secp256k1.zkp.generator`, the same reason
`btclib_secp256k1.zkp.context`'s own `__getattr__` docstring gives for
deferring its own `ctx` -- so that importing that module never reaches
for the extension on its own.
`pedersen_blind_sum` and `pedersen_blind_generator_blind_sum` answer a
blinding factor through `btclib_secp256k1._secret.take`, the same
function the primary package's own wrappers use, rather than through a
local equivalent: `take` overwrites through `ffi.buffer(buffer)` and
asks that buffer for its own length rather than typing it, so it does
not care which `ffi` built what it is reading, the same trade
`zkp.musig`'s own module docstring makes for `_secret.wipe`.
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import Any, overload
from btclib_secp256k1 import BytesLike, CData, MutableBytesLike
from btclib_secp256k1._scalar import in_range, octets, scalar, sequence
from btclib_secp256k1._secret import take, wipe
from . import context
__all__ = [
"generate",
"generate_blinded",
"h",
"parse",
"pedersen_blind_generator_blind_sum",
"pedersen_blind_sum",
"pedersen_commit",
"pedersen_commitment_parse",
"pedersen_commitment_serialize",
"pedersen_verify_tally",
"serialize",
]
_GENERATOR_SIZE = 33
_COMMITMENT_SIZE = 33
_BLIND_SIZE = 32
def _ptr_array(ffi: Any, cdecl: str, items: Sequence[CData]) -> CData:
"""Build the pointer array some calls require to be non-NULL.
Never `btclib_secp256k1._cdata.array`: that helper's own `ffi.new`
is mainline's, which has no `secp256k1_generator` or
`secp256k1_pedersen_commitment` in its cdef at all -- an
"undefined type name" `ffi.error`, measured rather than assumed, for
any array of either struct built through it. This subpackage's own
`ffi`, `context._bindings()` already resolved, is what every array here goes
through instead, whichever of the two element types it holds.
Unlike `context._array_or_null`, this never answers NULL for an
empty sequence: `secp256k1_pedersen_blind_sum`'s own header marks
its `blinds` array "(cannot be NULL)" with no exception for a zero
count, unlike `secp256k1_pedersen_verify_tally`'s `commits`, which
explicitly allows NULL where its count is zero. `ffi.new` of a
zero-length array is a real, non-NULL pointer to nothing, which is
what this needs.
Args:
ffi: this subpackage's own `ffi`, `context._bindings()` already
resolved.
cdecl: the cffi declaration of the array type.
items: the objects to point at, which the caller keeps alive.
Returns:
The array, never NULL.
"""
return ffi.new(cdecl, list(items))
[docs]
def parse(generator_bytes: BytesLike, name: str = "generator") -> CData:
"""Parse a 33-byte generator into its internal representation.
Args:
generator_bytes: the generator, 33 bytes, as `serialize`,
`generate`, `generate_blinded` or `h` answered it.
name: what the generator is, as the exception should call it.
Returns:
The libsecp256k1-zkp generator object.
Raises:
ValueError: if it is not 33 bytes, or not a valid generator.
"""
ffi, lib, ctx = context._bindings()
generator_bytes = octets(generator_bytes, name, _GENERATOR_SIZE)
gen = ffi.new("secp256k1_generator *")
if not lib.secp256k1_generator_parse(ctx, gen, generator_bytes):
raise ValueError(f"invalid {name}")
return gen
[docs]
def serialize(gen: CData) -> bytes:
"""Serialize a parsed generator.
Args:
gen: the libsecp256k1-zkp generator object, as `parse` returns.
Returns:
Its 33 bytes.
"""
ffi, lib, ctx = context._bindings()
output = ffi.new(f"char[{_GENERATOR_SIZE}]")
lib.secp256k1_generator_serialize(ctx, output, gen)
return bytes(ffi.unpack(output, _GENERATOR_SIZE))
[docs]
def h() -> bytes:
"""Return the static generator the header calls `h`, serialized.
"Static constant generator 'h' maintained for historical reasons",
in the header's own words: `secp256k1_generator_h`, read once rather
than parsed from a literal, is what lets a caller pin their own copy
of it against this library instead of against a copy of the bytes.
Returns:
Its 33 bytes.
"""
_ffi, lib, _ctx = context._bindings()
return serialize(lib.secp256k1_generator_h)
[docs]
def generate(seed32: BytesLike) -> bytes:
"""Generate a generator, distributed uniformly over the curve.
The result has no known discrete logarithm with respect to any other
generator this produces, or to the base generator `G`.
Args:
seed32: 32 bytes seeding the generator.
Returns:
Its 33-byte serialization.
Raises:
ValueError: if `seed32` is not 32 bytes.
RuntimeError: if libsecp256k1-zkp refuses the seed, which is
"highly unlikely" in the header's own words and cannot
happen for a seed this package generated.
"""
ffi, lib, ctx = context._bindings()
seed_bytes = octets(seed32, "seed32", 32)
gen = ffi.new("secp256k1_generator *")
if not lib.secp256k1_generator_generate(ctx, gen, seed_bytes):
msg = "generator generation failed"
raise RuntimeError(msg)
return serialize(gen)
[docs]
def generate_blinded(seed32: BytesLike, blind32: BytesLike | int) -> bytes:
"""Generate a generator, blinded by a secret tweak.
Equivalent to `generate` followed by a plain EC tweak of the result
read as a public key, and back -- computed directly instead, in one
call.
Args:
seed32: 32 bytes seeding the generator.
blind32: the secret value to blind it with, 32 bytes or an int
below 2**256.
Returns:
Its 33-byte serialization.
Raises:
ValueError: if `seed32` is not 32 bytes, if `blind32` is not 32
bytes or does not fit in them, if `blind32` is out of range,
or if libsecp256k1-zkp refuses the seed -- "highly unlikely"
in the header's own words and cannot happen for a seed this
package generated. The two failures share one return code
and are not told apart.
"""
ffi, lib, ctx = context._bindings()
seed_bytes = octets(seed32, "seed32", 32)
blind_bytes = scalar(blind32, "blind32")
gen = ffi.new("secp256k1_generator *")
if not lib.secp256k1_generator_generate_blinded(ctx, gen, seed_bytes, blind_bytes):
raise ValueError("invalid blind32, or seed generation failed")
return serialize(gen)
[docs]
def pedersen_commitment_parse(
commitment_bytes: BytesLike, name: str = "Pedersen commitment"
) -> CData:
"""Parse a 33-byte Pedersen commitment into its internal representation.
Args:
commitment_bytes: the commitment, 33 bytes, as
`pedersen_commitment_serialize` or `pedersen_commit`
answered it.
name: what the commitment is, as the exception should call it.
Returns:
The libsecp256k1-zkp Pedersen commitment object.
Raises:
ValueError: if it is not 33 bytes, or not a valid commitment.
"""
ffi, lib, ctx = context._bindings()
commitment_bytes = octets(commitment_bytes, name, _COMMITMENT_SIZE)
commit = ffi.new("secp256k1_pedersen_commitment *")
if not lib.secp256k1_pedersen_commitment_parse(ctx, commit, commitment_bytes):
raise ValueError(f"invalid {name}")
return commit
[docs]
def pedersen_commitment_serialize(commit: CData) -> bytes:
"""Serialize a parsed Pedersen commitment.
Args:
commit: the libsecp256k1-zkp commitment object, as
`pedersen_commitment_parse` returns.
Returns:
Its 33 bytes.
"""
ffi, lib, ctx = context._bindings()
output = ffi.new(f"char[{_COMMITMENT_SIZE}]")
lib.secp256k1_pedersen_commitment_serialize(ctx, output, commit)
return bytes(ffi.unpack(output, _COMMITMENT_SIZE))
[docs]
def pedersen_commit(
blind: BytesLike | int, value: int, gen_bytes: BytesLike | None = None
) -> bytes:
"""Commit to a value, `blind*G + value*gen`.
Args:
blind: the blinding factor, 32 bytes or an int below 2**256.
Generated and verified the same way as an ordinary
libsecp256k1 private key.
value: the value to commit to, an int in [0, 2**64).
gen_bytes: the generator `gen`, 33 bytes, or None for `h()` --
the plain, unblinded Pedersen commitment most callers want.
Returns:
The 33-byte serialized commitment.
Raises:
TypeError: if `value` is not an int.
ValueError: if `blind` is not 32 bytes, does not fit in them, or
is out of range; if `value` is a bool or does not fit in 8
bytes; or if `gen_bytes` is given and is not 33 bytes or not
a valid generator.
RuntimeError: if libsecp256k1-zkp refuses the blinding factor,
which is a ~2**-127 event for a random one and cannot happen
for a factor already verified.
"""
ffi, lib, ctx = context._bindings()
blind_bytes = scalar(blind, "blind")
if not isinstance(value, int):
raise TypeError(f"value must be an int, not {type(value).__name__}")
if isinstance(value, bool) or not 0 <= value < 2**64:
raise ValueError("value must be an int in [0, 2**64)")
gen_obj = (
lib.secp256k1_generator_h if gen_bytes is None else parse(gen_bytes, "gen")
)
commit = ffi.new("secp256k1_pedersen_commitment *")
if not lib.secp256k1_pedersen_commit(ctx, commit, blind_bytes, value, gen_obj):
msg = "Pedersen commitment failed"
raise RuntimeError(msg)
return pedersen_commitment_serialize(commit)
@overload
def pedersen_blind_sum(blinds: Sequence[BytesLike | int], npositive: int) -> bytes: ...
@overload
def pedersen_blind_sum(
blinds: Sequence[BytesLike | int], npositive: int, *, into: MutableBytesLike
) -> None: ...
@overload
def pedersen_blind_sum(
blinds: Sequence[BytesLike | int],
npositive: int,
*,
into: MutableBytesLike | None,
) -> bytes | None: ...
[docs]
def pedersen_blind_sum(
blinds: Sequence[BytesLike | int],
npositive: int,
*,
into: MutableBytesLike | None = None,
) -> bytes | None:
"""Sum blinding factors, first `npositive` positive, the rest negative.
Args:
blinds: the blinding factors, 32 bytes or an int below 2**256
each.
npositive: how many of `blinds`, from the start, are summed with
a positive sign; the rest are summed with a negative one.
into: a writable 32-byte buffer to receive the sum, instead of
the `bytes` this otherwise returns. See `_secret.take` and
SECURITY.md for what that does and does not buy.
Returns:
The 32-byte sum -- or None where `into` was given and holds it.
Raises:
TypeError: if `blinds` is a `str`, a cffi array of primitives or
a one-dimensional buffer of anything but byte-strings,
objects or structs (`bytes`, `bytearray`, `memoryview`,
`array.array`): one blinding factor, not a list of them. Also
if `npositive` is not an int, or if `into` is not a writable
buffer of contiguous one-dimensional octets.
ValueError: if one of `blinds` is not 32 bytes, does not fit in
them, or is out of range -- named by its position in the
sequence, `blinds[0]` being "blinding factor at index 0" --
if `npositive` is out of [0, len(blinds)], or if `into` is
not 32 bytes.
RuntimeError: if libsecp256k1-zkp refuses one of the factors,
which is a ~2**-127 event for a random one.
"""
ffi, lib, ctx = context._bindings()
sequence(blinds, "blinds")
npositive = in_range(npositive, "npositive", len(blinds))
# each buffer holds a copy of one of the caller's own blinding
# factors, and this package owns it: the `finally` takes that copy
# back whether the call answers or raises. The list is filled inside
# the `try` rather than before it, because the next element can be
# refused and the ones already copied are secrets by then --
# `silentpayments._create_outputs_` builds its own key lists that way
# for the same reason
blind_buffers: list[CData] = []
try:
blind_buffers.extend(
ffi.new(
f"unsigned char[{_BLIND_SIZE}]",
scalar(blind, f"blinding factor at index {i}"),
)
for i, blind in enumerate(blinds)
)
blind_out = ffi.new(f"char[{_BLIND_SIZE}]")
blinds_array = _ptr_array(ffi, "unsigned char *[]", blind_buffers)
if not lib.secp256k1_pedersen_blind_sum(
ctx, blind_out, blinds_array, len(blind_buffers), npositive
):
msg = "blinding factor sum failed"
raise RuntimeError(msg)
return take(blind_out, into=into)
finally:
for buffer in blind_buffers:
wipe(buffer)
[docs]
def pedersen_verify_tally(
commits_bytes: Sequence[BytesLike], ncommits_bytes: Sequence[BytesLike]
) -> bool:
"""Verify `commits` sums to `ncommits`, factors and values alike.
`sum(commits) - sum(ncommits) == 0`, for the generator each was
committed under: every commitment mixing more than one generator
must sum to zero for each of them separately, which is the caller's
to arrange by grouping commitments per generator before calling
this.
Args:
commits_bytes: the positive commitments, 33 bytes each.
ncommits_bytes: the negative commitments, 33 bytes each.
Returns:
True if they sum to zero.
Raises:
ValueError: if one of the commitments is not 33 bytes or not a
valid one -- named by its position in whichever sequence it
came from.
"""
ffi, lib, ctx = context._bindings()
commits = [
pedersen_commitment_parse(commit_bytes, f"commitment at index {i}")
for i, commit_bytes in enumerate(commits_bytes)
]
ncommits = [
pedersen_commitment_parse(commit_bytes, f"negative commitment at index {i}")
for i, commit_bytes in enumerate(ncommits_bytes)
]
return bool(
lib.secp256k1_pedersen_verify_tally(
ctx,
context._array_or_null(ffi, "secp256k1_pedersen_commitment *[]", commits),
len(commits),
context._array_or_null(ffi, "secp256k1_pedersen_commitment *[]", ncommits),
len(ncommits),
)
)
@overload
def pedersen_blind_generator_blind_sum(
values: Sequence[int],
generator_blinds: Sequence[BytesLike | int],
blinding_factors: Sequence[BytesLike | int],
n_inputs: int,
) -> bytes: ...
@overload
def pedersen_blind_generator_blind_sum(
values: Sequence[int],
generator_blinds: Sequence[BytesLike | int],
blinding_factors: Sequence[BytesLike | int],
n_inputs: int,
*,
into: MutableBytesLike,
) -> None: ...
@overload
def pedersen_blind_generator_blind_sum(
values: Sequence[int],
generator_blinds: Sequence[BytesLike | int],
blinding_factors: Sequence[BytesLike | int],
n_inputs: int,
*,
into: MutableBytesLike | None,
) -> bytes | None: ...
[docs]
def pedersen_blind_generator_blind_sum(
values: Sequence[int],
generator_blinds: Sequence[BytesLike | int],
blinding_factors: Sequence[BytesLike | int],
n_inputs: int,
*,
into: MutableBytesLike | None = None,
) -> bytes | None:
"""Correct the last blinding factor so every sum cancels.
For blinded generators `A' = A + r*G`, a Pedersen commitment
`P = v*A' + r'*G` really has the form `v*A + (v*r + r')*G`: this
subtracts the sum of `v*r + r'` over every element but the last from
the last element of `blinding_factors`, so that the total sum of
`(v*r + r')` over all of them is zero.
Args:
values: the asset values, one per commitment, an int in
[0, 2**64) each.
generator_blinds: the asset (generator) blinding factors, `r` in
the equation above, one per commitment, 32 bytes or an int
below 2**256 each.
blinding_factors: the commitment blinding factors, `r'` in the
equation above, one per commitment, 32 bytes or an int below
2**256 each -- every one but the last is used as given, and
only the last is what this recomputes.
n_inputs: how many of the initial elements, across all three
sequences, are negated in the final sum. Strictly less than
`len(values)`: the library's own `ARG_CHECK(n_total >
n_inputs)` refuses `n_inputs == len(values)`, the `0, 0` case
-- empty sequences included -- among them.
into: a writable 32-byte buffer to receive the corrected
blinding factor, instead of the `bytes` this otherwise
returns. See `_secret.take` and SECURITY.md for what that
does and does not buy.
Returns:
The corrected last blinding factor, 32 bytes -- the caller's own
`blinding_factors` with its last element replaced by this is the
array libsecp256k1-zkp's own in/out semantics describe -- or
None where `into` was given and holds it.
Raises:
TypeError: if `generator_blinds` or `blinding_factors` is a
`str`, a cffi array of primitives or a one-dimensional buffer
of anything but byte-strings, objects or structs (`bytes`,
`bytearray`, `memoryview`, `array.array`): one blinding
factor, not a list of them. Also if `n_inputs` is not an int,
if one of `values` is not an int, or if `into` is not a
writable buffer of contiguous one-dimensional octets.
ValueError: if the three sequences are not the same length, if
`n_inputs` is not in [0, len(values)), if one of `values` is
a bool or does not fit in 8 bytes, if one of the blinding
factors is not 32 bytes, does not fit in them, or is out of
range, or if `into` is not 32 bytes. A bad element is named
by its position in its own sequence, `values[0]` being
"value at index 0".
RuntimeError: if libsecp256k1-zkp refuses one of the factors,
which is a ~2**-127 event for random ones.
"""
ffi, lib, ctx = context._bindings()
sequence(generator_blinds, "generator_blinds")
sequence(blinding_factors, "blinding_factors")
n_total = len(values)
if len(generator_blinds) != n_total or len(blinding_factors) != n_total:
raise ValueError(
"values, generator_blinds and blinding_factors must match in length"
)
if not isinstance(n_inputs, int):
raise TypeError(f"n_inputs must be an int, not {type(n_inputs).__name__}")
if not 0 <= n_inputs < n_total:
raise ValueError("n_inputs must be in [0, len(values))")
# checked here rather than beside the two argument-wide checks above
# so that a bad element is reported in the order the signature
# declares the three sequences, the blinds' own `scalar` calls
# running as each array is built below
for i, value in enumerate(values):
if not isinstance(value, int):
raise TypeError(
f"value at index {i} must be an int, not {type(value).__name__}"
)
if isinstance(value, bool) or not 0 <= value < 2**64:
raise ValueError(f"value at index {i} must be an int in [0, 2**64)")
# this one is not wiped, where every blinding factor below is, and
# SECURITY.md's own *known and inherent* wipe bullet says so too:
# zeroing it would not take back the last copy. An amount reaches
# here only as a python `int`, `values` being a `Sequence[int]` and
# anything else refused above, so the object nothing can overwrite
# exists before the call. What makes the wipe below worth its
# `finally` is the opposite -- a caller may hand a blinding factor in
# as a cffi array and have no un-zeroizable copy of it made anywhere.
# `pedersen_commit` is the other side of the same point: it takes its
# own `value` as a plain C argument, with no buffer to wipe at all
value_array = ffi.new(f"uint64_t[{n_total}]", list(values))
# every element of both lists holds a copy of one of the caller's own
# blinding factors, and this package owns it: the `finally` takes
# every one of those copies back, whether the call answers or raises.
# Both lists are filled inside the `try` rather than before it,
# because the next element can be refused and the ones already copied
# are secrets by then -- `silentpayments._create_outputs_` builds its
# own key lists that way for the same reason
generator_blind_buffers: list[CData] = []
blinding_factor_buffers: list[CData] = []
try:
generator_blind_buffers.extend(
ffi.new(
f"unsigned char[{_BLIND_SIZE}]",
scalar(blind, f"generator blind at index {i}"),
)
for i, blind in enumerate(generator_blinds)
)
# unsigned char, as the generator blinds above: `ffi.new` takes a
# cdata initializer only where the item type matches, and
# `_scalar._owned_octets` re-views a caller's own octets as
# `unsigned char[32]`, so the declaration is what decides whether
# a caller holding a blinding factor in memory of their own is
# accepted here on the same terms as beside it. It decides
# nothing else here: `_secret.take` goes through `ffi.buffer`,
# which asks the buffer for its length rather than its type, and
# `_ptr_array` takes either into an `unsigned char *[]`
blinding_factor_buffers.extend(
ffi.new(
f"unsigned char[{_BLIND_SIZE}]",
scalar(blind, f"blinding factor at index {i}"),
)
for i, blind in enumerate(blinding_factors)
)
# never NULL: n_total is at least 1 here, `0 <= n_inputs < n_total`
# having already refused the only n_total for which the library's
# own arrays may be NULL
generator_blind_array = _ptr_array(
ffi, "unsigned char *[]", generator_blind_buffers
)
blinding_factor_array = _ptr_array(
ffi, "unsigned char *[]", blinding_factor_buffers
)
if not lib.secp256k1_pedersen_blind_generator_blind_sum(
ctx,
value_array,
generator_blind_array,
blinding_factor_array,
n_total,
n_inputs,
):
msg = "blinding factor correction failed"
raise RuntimeError(msg)
return take(blinding_factor_buffers[-1], into=into)
finally:
# the last blinding-factor buffer is in this loop too: `take`
# leaves it zeroed on the way out of a call that reached it, and
# a second wipe of zeros is what covers the call that did not
for buffer in (*generator_blind_buffers, *blinding_factor_buffers):
wipe(buffer)