You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
146 lines
5.0 KiB
146 lines
5.0 KiB
from __future__ import annotations |
|
|
|
from typing import List, Tuple |
|
|
|
from ..secp256k1lab.secp256k1 import GE, G, Scalar |
|
from ..secp256k1lab.util import tagged_hash |
|
|
|
from .util import tagged_hash_bip_dkg |
|
|
|
|
|
class Polynomial: |
|
# A scalar polynomial. |
|
# |
|
# A polynomial f of degree at most t - 1 is represented by a list `coeffs` |
|
# of t coefficients, i.e., f(x) = coeffs[0] + ... + coeffs[t-1] * |
|
# x^(t-1).""" |
|
coeffs: List[Scalar] |
|
|
|
def __init__(self, coeffs: List[Scalar]) -> None: |
|
self.coeffs = coeffs |
|
|
|
def eval(self, x: Scalar) -> Scalar: |
|
# Evaluate a polynomial at position x. |
|
|
|
value = Scalar(0) |
|
# Reverse coefficients to compute evaluation via Horner's method |
|
for coeff in self.coeffs[::-1]: |
|
value = value * x + coeff |
|
return value |
|
|
|
def __call__(self, x: Scalar) -> Scalar: |
|
return self.eval(x) |
|
|
|
|
|
class VSSCommitment: |
|
ges: List[GE] |
|
|
|
def __init__(self, ges: List[GE]) -> None: |
|
self.ges = ges |
|
|
|
def t(self) -> int: |
|
return len(self.ges) |
|
|
|
def pubshare(self, i: int) -> GE: |
|
pubshare: GE = GE.batch_mul( |
|
*(((i + 1) ** j, self.ges[j]) for j in range(0, len(self.ges))) |
|
) |
|
return pubshare |
|
|
|
@staticmethod |
|
def verify_secshare(secshare: Scalar, pubshare: GE) -> bool: |
|
# The caller needs to provide the correct pubshare(i) |
|
actual = secshare * G |
|
valid: bool = actual == pubshare |
|
return valid |
|
|
|
def to_bytes(self) -> bytes: |
|
# Return commitments to the coefficients of f. |
|
return b"".join([ge.to_bytes_compressed_with_infinity() for ge in self.ges]) |
|
|
|
def __add__(self, other: VSSCommitment) -> VSSCommitment: |
|
assert self.t() == other.t() |
|
return VSSCommitment([self.ges[i] + other.ges[i] for i in range(self.t())]) |
|
|
|
@staticmethod |
|
def from_bytes_and_t(b: bytes, t: int) -> VSSCommitment: |
|
if len(b) != 33 * t: |
|
raise ValueError |
|
ges = [GE.from_bytes_compressed(b[i : i + 33]) for i in range(0, 33 * t, 33)] |
|
return VSSCommitment(ges) |
|
|
|
def commitment_to_secret(self) -> GE: |
|
return self.ges[0] |
|
|
|
def commitment_to_nonconst_terms(self) -> List[GE]: |
|
return self.ges[1 : self.t()] |
|
|
|
def invalid_taproot_commit(self) -> Tuple[VSSCommitment, Scalar, GE]: |
|
# Return a modified VSS commitment such that the threshold public key |
|
# generated from it has an unspendable BIP 341 Taproot script path. |
|
# |
|
# Specifically, for a VSS commitment `com`, we have: |
|
# `com.invalid_taproot_commit().commitment_to_secret() = com.commitment_to_secret() + t*G`. |
|
# |
|
# The tweak `t` commits to an empty message, which is invalid according |
|
# to BIP 341 for Taproot script spends. This follows BIP 341's |
|
# recommended approach for committing to an unspendable script path. |
|
# |
|
# This prevents a malicious participant from secretly inserting a *valid* |
|
# Taproot commitment to a script path into the summed VSS commitment during |
|
# the DKG protocol. If the resulting threshold public key was used directly |
|
# in a BIP 341 Taproot output, the malicious participant would be able to |
|
# spend the output using their hidden script path. |
|
# |
|
# The function returns the updated VSS commitment and the tweak `t` which |
|
# must be added to all secret shares of the commitment. |
|
pk = self.commitment_to_secret() |
|
secshare_tweak = Scalar.from_bytes_checked( |
|
tagged_hash("TapTweak", pk.to_bytes_compressed()) |
|
) |
|
pubshare_tweak = secshare_tweak * G |
|
vss_tweak = VSSCommitment([pubshare_tweak] + [GE()] * (self.t() - 1)) |
|
return (self + vss_tweak, secshare_tweak, pubshare_tweak) |
|
|
|
|
|
class VSS: |
|
f: Polynomial |
|
|
|
def __init__(self, f: Polynomial) -> None: |
|
self.f = f |
|
|
|
@staticmethod |
|
def generate(seed: bytes, t: int) -> VSS: |
|
coeffs = [ |
|
Scalar.from_bytes_checked( |
|
tagged_hash_bip_dkg("vss coeffs", seed + i.to_bytes(4, byteorder="big")) |
|
) |
|
for i in range(t) |
|
] |
|
return VSS(Polynomial(coeffs)) |
|
|
|
def secshare_for(self, i: int) -> Scalar: |
|
# Return the secret share for the participant with index i. |
|
# |
|
# This computes f(i+1). |
|
if i < 0: |
|
raise ValueError(f"Invalid participant index: {i}") |
|
x = Scalar(i + 1) |
|
# Ensure we don't compute f(0), which is the secret. |
|
assert x != Scalar(0) |
|
return self.f(x) |
|
|
|
def secshares(self, n: int) -> List[Scalar]: |
|
# Return the secret shares for the participants with indices 0..n-1. |
|
# |
|
# This computes [f(1), ..., f(n)]. |
|
return [self.secshare_for(i) for i in range(0, n)] |
|
|
|
def commit(self) -> VSSCommitment: |
|
return VSSCommitment([c * G for c in self.f.coeffs]) |
|
|
|
def secret(self) -> Scalar: |
|
# Return the secret to be shared. |
|
# |
|
# This computes f(0). |
|
return self.f.coeffs[0]
|
|
|