mirror of
https://github.com/luxfi/lamport.git
synced 2026-10-30 16:21:45 +00:00
199 lines
6.7 KiB
Go
199 lines
6.7 KiB
Go
// Package threshold provides T-Chain MPC integration for Lamport signatures.
|
|
//
|
|
// The key insight is that threshold control lives OFF-CHAIN in the T-Chain MPC network.
|
|
// On-chain sees ONE standard Lamport signature - works on ANY EVM chain without precompiles!
|
|
//
|
|
// SECURITY MODEL:
|
|
// - Threshold property (t-of-n) enforced by T-Chain MPC network
|
|
// - Each MPC node holds shares of ONE Lamport private key
|
|
// - Partial signatures aggregated to form complete Lamport signature
|
|
// - On-chain verifies normal keccak256 hashes
|
|
//
|
|
// ATTACK MITIGATIONS:
|
|
// - Canonical digest: Every node computes safeTxHash locally
|
|
// - 1-round digest agreement: Broadcast H(safeTxHash) BEFORE revealing material
|
|
// - Domain separation: chainId + moduleAddress prevent replay
|
|
// - One-time keys: nextPKH rotation after each signature
|
|
//
|
|
// See: LP-4105 (Lamport OTS for Lux Safe)
|
|
package threshold
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"io"
|
|
|
|
"github.com/luxfi/lamport/primitives"
|
|
)
|
|
|
|
// Config holds configuration for threshold MPC Lamport signing.
|
|
type Config struct {
|
|
// Threshold is the minimum number of parties needed to sign (t in t-of-n)
|
|
Threshold int
|
|
|
|
// TotalParties is the total number of parties (n in t-of-n)
|
|
TotalParties int
|
|
|
|
// PartyID is this party's identifier
|
|
PartyID string
|
|
|
|
// ChainID for domain separation (prevents cross-chain replay)
|
|
ChainID uint64
|
|
|
|
// ModuleAddress for domain separation (prevents cross-contract replay)
|
|
ModuleAddress [20]byte
|
|
}
|
|
|
|
// Share represents a party's share of a Lamport private key.
|
|
// In threshold Lamport, each party holds shares of the preimages.
|
|
type Share struct {
|
|
// PartyID identifies which party owns this share
|
|
PartyID string
|
|
|
|
// PreimageShares holds this party's shares for each bit position
|
|
// For simple additive sharing: sum of all shares = actual preimage
|
|
PreimageShares [primitives.KeyBits][2][primitives.PreimageSize]byte
|
|
|
|
// Index is this party's index (1 to n)
|
|
Index int
|
|
}
|
|
|
|
// PartialSignature is a party's contribution to the threshold signature.
|
|
type PartialSignature struct {
|
|
// PartyID identifies which party created this partial
|
|
PartyID string
|
|
|
|
// Index is the party's index
|
|
Index int
|
|
|
|
// PreimagePartials contains the revealed partial preimages
|
|
// Only the bits corresponding to the message are populated
|
|
PreimagePartials [primitives.KeyBits][primitives.PreimageSize]byte
|
|
|
|
// BitMask indicates which bits were included (for verification)
|
|
BitMask [32]byte
|
|
}
|
|
|
|
// DigestCommitment is used for 1-round digest agreement.
|
|
// Each party broadcasts H(safeTxHash) BEFORE revealing any signing material.
|
|
type DigestCommitment struct {
|
|
PartyID string
|
|
Commitment [32]byte // H(safeTxHash || partyID)
|
|
}
|
|
|
|
var (
|
|
// ErrInvalidThreshold indicates invalid threshold parameters
|
|
ErrInvalidThreshold = errors.New("threshold: invalid threshold (must be 1 <= t <= n)")
|
|
|
|
// ErrNotEnoughParties indicates insufficient parties for threshold
|
|
ErrNotEnoughParties = errors.New("threshold: not enough parties to meet threshold")
|
|
|
|
// ErrDigestMismatch indicates parties disagreed on the message
|
|
ErrDigestMismatch = errors.New("threshold: digest mismatch - parties disagree on message")
|
|
|
|
// ErrInvalidPartial indicates a partial signature failed verification
|
|
ErrInvalidPartial = errors.New("threshold: invalid partial signature")
|
|
)
|
|
|
|
// NewConfig creates a new threshold configuration.
|
|
func NewConfig(threshold, totalParties int, partyID string, chainID uint64, moduleAddr [20]byte) (*Config, error) {
|
|
if threshold < 1 || threshold > totalParties {
|
|
return nil, ErrInvalidThreshold
|
|
}
|
|
return &Config{
|
|
Threshold: threshold,
|
|
TotalParties: totalParties,
|
|
PartyID: partyID,
|
|
ChainID: chainID,
|
|
ModuleAddress: moduleAddr,
|
|
}, nil
|
|
}
|
|
|
|
// ComputeMessage computes the domain-separated message for threshold signing.
|
|
// This MUST be computed locally by each party - never accept from coordinator!
|
|
func (c *Config) ComputeMessage(safeTxHash, nextPKH [32]byte) [32]byte {
|
|
return primitives.ComputeThresholdMessage(safeTxHash, nextPKH, c.ModuleAddress, c.ChainID)
|
|
}
|
|
|
|
// CreateDigestCommitment creates a commitment to the safeTxHash.
|
|
// This is broadcast in round 1 before any signing material is revealed.
|
|
func (c *Config) CreateDigestCommitment(safeTxHash [32]byte) DigestCommitment {
|
|
// Commitment = H(safeTxHash || partyID)
|
|
h := primitives.Keccak256Multi(safeTxHash[:], []byte(c.PartyID))
|
|
return DigestCommitment{
|
|
PartyID: c.PartyID,
|
|
Commitment: h,
|
|
}
|
|
}
|
|
|
|
// VerifyDigestCommitment verifies another party's commitment matches the expected digest.
|
|
func VerifyDigestCommitment(commitment DigestCommitment, safeTxHash [32]byte) bool {
|
|
expected := primitives.Keccak256Multi(safeTxHash[:], []byte(commitment.PartyID))
|
|
return commitment.Commitment == expected
|
|
}
|
|
|
|
// GenerateShares generates n shares of a Lamport private key for threshold signing.
|
|
// This uses simple additive secret sharing.
|
|
//
|
|
// For Shamir-style sharing with t-of-n, use GenerateSharesShamir.
|
|
func GenerateShares(n int) ([]*Share, *primitives.PublicKey, error) {
|
|
return GenerateSharesFromReader(n, rand.Reader)
|
|
}
|
|
|
|
// GenerateSharesFromReader generates shares using a specific random source.
|
|
func GenerateSharesFromReader(n int, random io.Reader) ([]*Share, *primitives.PublicKey, error) {
|
|
shares := make([]*Share, n)
|
|
pub := &primitives.PublicKey{}
|
|
|
|
// For each bit position
|
|
for i := 0; i < primitives.KeyBits; i++ {
|
|
for bit := 0; bit < 2; bit++ {
|
|
// Generate n-1 random shares
|
|
var actualPreimage [primitives.PreimageSize]byte
|
|
if _, err := io.ReadFull(random, actualPreimage[:]); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Compute public key hash
|
|
pub.Hashes[i][bit] = primitives.Keccak256(actualPreimage[:])
|
|
|
|
// Create shares: n-1 random, last one = preimage - sum(others)
|
|
var sum [primitives.PreimageSize]byte
|
|
for j := 0; j < n-1; j++ {
|
|
if shares[j] == nil {
|
|
shares[j] = &Share{Index: j + 1}
|
|
}
|
|
if _, err := io.ReadFull(random, shares[j].PreimageShares[i][bit][:]); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
// Add to sum
|
|
for k := 0; k < primitives.PreimageSize; k++ {
|
|
sum[k] ^= shares[j].PreimageShares[i][bit][k]
|
|
}
|
|
}
|
|
|
|
// Last share = preimage XOR sum(others)
|
|
if shares[n-1] == nil {
|
|
shares[n-1] = &Share{Index: n}
|
|
}
|
|
for k := 0; k < primitives.PreimageSize; k++ {
|
|
shares[n-1].PreimageShares[i][bit][k] = actualPreimage[k] ^ sum[k]
|
|
}
|
|
}
|
|
}
|
|
|
|
return shares, pub, nil
|
|
}
|
|
|
|
// ReconstructPreimage reconstructs a preimage from shares (for the needed bits only).
|
|
// In the MPC protocol, this happens in the aggregation phase.
|
|
func ReconstructPreimage(shares []*Share, bitIndex int, bitValue int) [primitives.PreimageSize]byte {
|
|
var result [primitives.PreimageSize]byte
|
|
for _, share := range shares {
|
|
for k := 0; k < primitives.PreimageSize; k++ {
|
|
result[k] ^= share.PreimageShares[bitIndex][bitValue][k]
|
|
}
|
|
}
|
|
return result
|
|
}
|