diff --git a/.gitignore b/.gitignore index de985fec..70bd43c8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,8 @@ cli_output_data.log /solidity /cfgBuilder/*.toml /lvldbdata + +# Rust FFI build artifacts (regenerated by `make build-ffi`) +rust/target/ +rust/lib/ +rust/include/ diff --git a/Makefile b/Makefile index 8e2cc586..34d9e0ab 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ -.PHONY: help run build install license example e2e-test +.PHONY: help run build install license example e2e-test \ + build-ffi build-ffi-debug clean-ffi all: help export GOLANG_PROTOBUF_REGISTRATION_CONFLICT=ignore @@ -61,3 +62,32 @@ $(PLATFORMS): GOOS=$(os) GOARCH=$(arch) go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=ignore" -o 'build/${os}-${arch}/relayer'; \ build-all: $(PLATFORMS) + +# ── Rust FFI (cggmp21) ──────────────────────────────────────────────────────── +# Builds the cggmp21-ffi Rust crate as a static library for CGo consumption. +# Targets linux/amd64 only (we build inside the Docker image). +# Outputs: rust/lib/libcggmp21_ffi.a and rust/include/cggmp21.h +FFI_MANIFEST := rust/Cargo.toml +FFI_CRATE := cggmp21-ffi +FFI_OUT_LIB := rust/lib +FFI_OUT_INCLUDE := rust/include +FFI_HEADER_SRC := rust/cggmp21-ffi/include/cggmp21.h + +## build-ffi: Build cggmp21-ffi as a release static library and stage it for CGo. +build-ffi: + cargo build --release --manifest-path $(FFI_MANIFEST) -p $(FFI_CRATE) + mkdir -p $(FFI_OUT_LIB) $(FFI_OUT_INCLUDE) + cp rust/target/release/libcggmp21_ffi.a $(FFI_OUT_LIB)/ + cp $(FFI_HEADER_SRC) $(FFI_OUT_INCLUDE)/ + +## build-ffi-debug: Same as build-ffi but unoptimised (faster to build). +build-ffi-debug: + cargo build --manifest-path $(FFI_MANIFEST) -p $(FFI_CRATE) + mkdir -p $(FFI_OUT_LIB) $(FFI_OUT_INCLUDE) + cp rust/target/debug/libcggmp21_ffi.a $(FFI_OUT_LIB)/ + cp $(FFI_HEADER_SRC) $(FFI_OUT_INCLUDE)/ + +## clean-ffi: Remove staged FFI artifacts and the Rust target directory. +clean-ffi: + cargo clean --manifest-path $(FFI_MANIFEST) -p $(FFI_CRATE) + rm -rf $(FFI_OUT_LIB) $(FFI_OUT_INCLUDE) diff --git a/keyshare/cggmp.go b/keyshare/cggmp.go new file mode 100644 index 00000000..7a9ba1dc --- /dev/null +++ b/keyshare/cggmp.go @@ -0,0 +1,233 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package keyshare + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math/big" +) + +// CggmpShare returns this key share serialised as a cggmp21 KeyShare +// JSON value, suitable for passing to tss/cggmp21.NewSigningSession (or any of +// the cggmp21 FFI entry points). +// +// The conversion is a re-encoding of the mathematical state stored by tss-lib +// (a.k.a. ChainSafe/threshlib) into cggmp21's serde JSON shape. Both libraries +// implement GG18/CGGMP-style threshold ECDSA on secp256k1 with the same +// per-party data: +// +// - Secret share x_i ← LocalSecrets.Xi +// - Public shares X_j ← LocalPartySaveData.BigXj +// - Shared public key ← LocalPartySaveData.ECDSAPub +// - VSS evaluation points ← LocalPartySaveData.Ks +// - Paillier modulus N ← LocalPartySaveData.NTildej (see note below) +// - Paillier secret primes ← 2·LocalPreParams.P+1, 2·LocalPreParams.Q+1 +// - Ring-Pedersen (s, t) ← LocalPartySaveData.{H1j, H2j} +// +// Note on the Paillier modulus: cggmp21 requires the Paillier modulus and the +// Ring-Pedersen modulus to be the SAME number (`aux.p * aux.q == aux.parties[i].N`). +// tss-lib/threshlib generates them as TWO separate moduli (PaillierSK.N and +// NTildej[i]). We discard the original Paillier modulus and use NTildej as the +// unified modulus for cggmp21; `aux.{p, q}` are the safe primes that factor +// NTildei (recovered from LocalPreParams.{P, Q}, which threshlib stores as +// Sophie-Germain primes: SafePrime = 2·p_sg + 1). +// +// This substitution is sound because the cryptographic identity of the share is +// determined by the SECRET share x_i and the SHARED PUBLIC KEY — both are +// preserved exactly. The Paillier setup is fresh signing-protocol state; +// signatures produced from converted shares verify against the original +// ECDSAPub. +// +// Returns an error if the source share is missing required fields. +func (k ECDSAKeyshare) CggmpShare() ([]byte, error) { + pre := k.Key.LocalPreParams + if !pre.ValidateWithProof() { + return nil, errors.New("keyshare: incomplete pre-params (missing Paillier SK, NTildei, H1/H2, or Sophie-Germain P/Q)") + } + if k.Key.Xi == nil || k.Key.ShareID == nil || k.Key.ECDSAPub == nil { + return nil, errors.New("keyshare: missing core secrets (Xi/ShareID/ECDSAPub)") + } + + n := len(k.Key.Ks) + if n == 0 || len(k.Key.BigXj) != n || len(k.Key.NTildej) != n || + len(k.Key.H1j) != n || len(k.Key.H2j) != n { + return nil, fmt.Errorf("keyshare: party arrays have inconsistent lengths (Ks=%d, BigXj=%d, NTildej=%d)", + n, len(k.Key.BigXj), len(k.Key.NTildej)) + } + + myIndex := -1 + for j, kj := range k.Key.Ks { + if kj != nil && kj.Cmp(k.Key.ShareID) == 0 { + myIndex = j + break + } + } + if myIndex < 0 { + return nil, errors.New("keyshare: this party's ShareID not found in Ks") + } + + // Recover the safe primes that factor NTildei from threshlib's stored + // Sophie-Germain primes (P, Q). By construction: NTildei = (2P+1)·(2Q+1). + one := big.NewInt(1) + two := big.NewInt(2) + safeP := new(big.Int).Add(new(big.Int).Mul(two, k.Key.P), one) + safeQ := new(big.Int).Add(new(big.Int).Mul(two, k.Key.Q), one) + + // Sanity check — the safe primes must factor NTildei, the modulus we'll be + // using as cggmp21's unified Paillier+Ring-Pedersen modulus. + if k.Key.NTildei == nil { + return nil, errors.New("keyshare: missing NTildei") + } + if new(big.Int).Mul(safeP, safeQ).Cmp(k.Key.NTildei) != 0 { + return nil, errors.New("keyshare: safe-prime recovery failed (2P+1)(2Q+1) ≠ NTildei — share has unexpected pre-params layout") + } + + // Build the public-shares list. + publicShares := make([]string, n) + for j, bigX := range k.Key.BigXj { + if bigX == nil { + return nil, fmt.Errorf("keyshare: BigXj[%d] is nil", j) + } + hexPt, err := compressPoint(bigX.X(), bigX.Y()) + if err != nil { + return nil, fmt.Errorf("keyshare: BigXj[%d]: %w", j, err) + } + publicShares[j] = hexPt + } + + sharedPubHex, err := compressPoint(k.Key.ECDSAPub.X(), k.Key.ECDSAPub.Y()) + if err != nil { + return nil, fmt.Errorf("keyshare: ECDSAPub: %w", err) + } + + // VSS evaluation points (Ks). cggmp21 stores them as 32-byte hex scalars. + iList := make([]string, n) + for j, ks := range k.Key.Ks { + if ks == nil { + return nil, fmt.Errorf("keyshare: Ks[%d] is nil", j) + } + iList[j] = scalarHex(ks) + } + + // Per-party aux info. + // Ring-Pedersen mapping: tss-lib computes H2 = H1^alpha (alpha secret), + // cggmp21 stores s = t^lambda (lambda secret) — so tss-lib's H1 plays the + // role of cggmp21's t (generator), and H2 plays the role of s (= t^lambda). + parties := make([]cggmpPartyAux, n) + for j := 0; j < n; j++ { + parties[j] = cggmpPartyAux{ + N: bigInt16(k.Key.NTildej[j]), + S: bigInt16(k.Key.H2j[j]), + T: bigInt16(k.Key.H1j[j]), + } + } + + // threshlib's signing threshold is `t` where t+1 parties cooperate. + // cggmp21's min_signers is the count of required signers, i.e. t+1. + minSigners := uint16(k.Threshold + 1) + + share := cggmpKeyShareJSON{ + Core: cggmpCoreJSON{ + Curve: "secp256k1", + I: uint16(myIndex), + SharedPublicKey: sharedPubHex, + PublicShares: publicShares, + VssSetup: &cggmpVssJSON{ + MinSigners: minSigners, + I: iList, + }, + X: scalarHex(k.Key.Xi), + }, + Aux: cggmpAuxJSON{ + P: bigInt16(safeP), + Q: bigInt16(safeQ), + Parties: parties, + }, + } + return json.Marshal(share) +} + +// ── cggmp21 KeyShare JSON shape (matches the serde output) ──────── + +type cggmpKeyShareJSON struct { + Core cggmpCoreJSON `json:"core"` + Aux cggmpAuxJSON `json:"aux"` +} + +type cggmpCoreJSON struct { + Curve string `json:"curve"` + I uint16 `json:"i"` + SharedPublicKey string `json:"shared_public_key"` + PublicShares []string `json:"public_shares"` + VssSetup *cggmpVssJSON `json:"vss_setup,omitempty"` + X string `json:"x"` +} + +type cggmpVssJSON struct { + MinSigners uint16 `json:"min_signers"` + I []string `json:"I"` +} + +type cggmpAuxJSON struct { + P cggmpBigInt `json:"p"` + Q cggmpBigInt `json:"q"` + Parties []cggmpPartyAux `json:"parties"` +} + +type cggmpBigInt struct { + Radix int `json:"radix"` + Value string `json:"value"` +} + +type cggmpPartyAux struct { + N cggmpBigInt `json:"N"` + S cggmpBigInt `json:"s"` + T cggmpBigInt `json:"t"` +} + +// ── Encoding helpers ───────────────────────────────────────────────────────── + +// secp256k1 curve order N (= 2^256 - 0x14551231950b75fc4402da1732fc9bebf). +// Used to reduce scalars before encoding. +var secp256k1N, _ = new(big.Int).SetString( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16) + +// scalarHex returns x mod N as a fixed 32-byte big-endian lowercase hex string. +func scalarHex(x *big.Int) string { + r := new(big.Int).Mod(x, secp256k1N) + buf := make([]byte, 32) + r.FillBytes(buf) + return hex.EncodeToString(buf) +} + +// compressPoint returns the 33-byte SEC1 compressed encoding of (x, y) as hex. +// Parity prefix: 0x02 if y is even, 0x03 if y is odd. +func compressPoint(x, y *big.Int) (string, error) { + if x == nil || y == nil { + return "", errors.New("nil point coordinate") + } + xb := make([]byte, 32) + x.FillBytes(xb) + prefix := byte(0x02) + if y.Bit(0) == 1 { + prefix = 0x03 + } + out := make([]byte, 33) + out[0] = prefix + copy(out[1:], xb) + return hex.EncodeToString(out), nil +} + +// bigInt16 wraps a non-negative big.Int as cggmp21's {"radix":16, "value":""} +// JSON shape used for Paillier moduli and Ring-Pedersen parameters. +func bigInt16(x *big.Int) cggmpBigInt { + if x == nil { + return cggmpBigInt{Radix: 16, Value: "0"} + } + // rug::Integer JSON omits the leading "0x" and uses lowercase. + return cggmpBigInt{Radix: 16, Value: x.Text(16)} +} diff --git a/keyshare/cggmp_test.go b/keyshare/cggmp_test.go new file mode 100644 index 00000000..85bcc4ce --- /dev/null +++ b/keyshare/cggmp_test.go @@ -0,0 +1,254 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package keyshare_test + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "os" + "path/filepath" + "sort" + "testing" + + "github.com/binance-chain/tss-lib/ecdsa/keygen" + "github.com/ethereum/go-ethereum/crypto" + "github.com/sprintertech/sprinter-signing/keyshare" + "github.com/sprintertech/sprinter-signing/tss/cggmp21" +) + +// loadTssLibFixture reads one of the existing tss-lib test keyshares and +// rebuilds it as a keyshare.ECDSAKeyshare so the conversion can run on it. +// +// The keyshare files are tss-lib LocalPartySaveData wrapped with metadata — +// stored at tss/test/keyshares/.keyshare. They were produced by the project's +// pre-cggmp21 keygen flow. +func loadTssLibFixture(t *testing.T, i int) keyshare.ECDSAKeyshare { + t.Helper() + path := filepath.Join("..", "tss", "test", "keyshares", fmt.Sprintf("%d.keyshare", i)) + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("load fixture %s: %v", path, err) + } + var k keyshare.ECDSAKeyshare + if err := json.Unmarshal(b, &k); err != nil { + t.Fatalf("unmarshal fixture %s: %v", path, err) + } + if k.Key.Xi == nil { + t.Skipf("fixture %s is an empty/non-participant share — skipping", path) + } + return k +} + +// TestCggmpShare_ConvertAcceptedByFFI confirms that the cggmp21 FFI accepts the +// converted share (its Deserialize+Validate path runs full structural and +// cryptographic checks: Paillier modulus size, gcd(s, N), gcd(t, N), point +// non-zero, etc.). If the conversion produces malformed JSON or invalid params, +// the FFI returns an error and this test fails. +func TestCggmpShare_ConvertAcceptedByFFI(t *testing.T) { + for i := 0; i < 3; i++ { + i := i + t.Run(fmt.Sprintf("share-%d", i), func(t *testing.T) { + k := loadTssLibFixture(t, i) + shareJSON, err := k.CggmpShare() + if err != nil { + t.Fatalf("CggmpShare(): %v", err) + } + + // Smoke-test by constructing a session — the FFI deserialises and + // runs Validate on the share. If anything in the conversion is + // wrong, this errors out with a useful message. + signers := []uint16{0, 1} + eid := []byte("cggmp21-conversion-validate") + hash := sha256.Sum256([]byte("x")) + s, err := cggmp21.NewSigningSession(shareJSON, eid, 0, signers, hash[:]) + if err != nil { + // The FFI may still reject parties_indexes when i != 0 in the + // signing set, but a deserialise/Validate failure surfaces here + // with "deserialize"/"InvalidKeyShare" in the message. + t.Fatalf("FFI rejected converted share: %v\nshare=%s", err, string(shareJSON)) + } + s.Close() + }) + } +} + +// TestCggmpShare_SigningE2E proves end-to-end correctness: converts all three +// tss-lib fixtures, runs 2-of-3 cggmp21 signing between two of them, and +// verifies the resulting signature against the public key recorded in the +// original tss-lib share. +// +// If the conversion mis-encodes ANY of: secret share, public shares, VSS setup, +// or Paillier/Ring-Pedersen aux, the protocol will either error during signing +// or produce a signature that doesn't verify. So this is the real correctness +// test — passing it means the conversion is sound. +func TestCggmpShare_SigningE2E(t *testing.T) { + type signerSlot struct { + shareJSON []byte + keygenIndex uint16 + } + slots := make([]signerSlot, 0, 3) + var pubkeyCompressed []byte + + for i := 0; i < 3; i++ { + k := loadTssLibFixture(t, i) + shareJSON, err := k.CggmpShare() + if err != nil { + t.Fatalf("CggmpShare share-%d: %v", i, err) + } + // Find this party's keygen index (position of ShareID in the sorted Ks list). + // The fixture file numbering (0/1/2) doesn't match keygen index. + idx := -1 + for j, kj := range k.Key.Ks { + if kj.Cmp(k.Key.ShareID) == 0 { + idx = j + break + } + } + if idx < 0 { + t.Fatalf("share-%d: ShareID not in Ks", i) + } + slots = append(slots, signerSlot{shareJSON: shareJSON, keygenIndex: uint16(idx)}) + + if pubkeyCompressed == nil { + pubX, pubY := k.Key.ECDSAPub.X(), k.Key.ECDSAPub.Y() + pubkeyCompressed = make([]byte, 33) + if pubY.Bit(0) == 0 { + pubkeyCompressed[0] = 0x02 + } else { + pubkeyCompressed[0] = 0x03 + } + xb := make([]byte, 32) + pubX.FillBytes(xb) + copy(pubkeyCompressed[1:], xb) + } + } + + // Sort by keygen index so the signing-position ↔ keygen-index mapping is + // deterministic and parties_indexes is monotonic (cggmp21 requirement). + sort.Slice(slots, func(a, b int) bool { return slots[a].keygenIndex < slots[b].keygenIndex }) + + // Pick the first two signers — keygen indexes ascending. parties_indexes is + // passed to every signer and lists the keygen indexes of all participants. + signerSet := []uint16{slots[0].keygenIndex, slots[1].keygenIndex} + + eid := []byte("cggmp21-conversion-e2e-signing") + hash := sha256.Sum256([]byte("hello from a converted tss-lib share")) + + s0, err := cggmp21.NewSigningSession(slots[0].shareJSON, eid, 0, signerSet, hash[:]) + if err != nil { + t.Fatalf("signer 0 (keygen idx %d) NewSigningSession: %v", slots[0].keygenIndex, err) + } + defer s0.Close() + s1, err := cggmp21.NewSigningSession(slots[1].shareJSON, eid, 1, signerSet, hash[:]) + if err != nil { + t.Fatalf("signer 1 (keygen idx %d) NewSigningSession: %v", slots[1].keygenIndex, err) + } + defer s1.Close() + + runSigning(t, []*cggmp21.SigningSession{s0, s1}) + + sig, err := s0.Signature() + if err != nil { + // Surface signer-1's view too in case it errored. + if sig1, err1 := s1.Signature(); err1 != nil { + t.Logf("signer 1 also errored: %v (sig=%x)", err1, sig1) + } + t.Fatalf("signer 0 Signature: %v", err) + } + + if !crypto.VerifySignature(pubkeyCompressed, hash[:], sig) { + t.Fatalf("converted-share signature does not verify\n sig=%x\n hash=%x\n pubkey=%x", + sig, hash[:], pubkeyCompressed) + } +} + +// runSigning is a minimal in-process message router that drives the two +// SigningSessions to completion. +func runSigning(t *testing.T, sessions []*cggmp21.SigningSession) { + t.Helper() + type pending struct { + sender uint16 + broadcast bool + payload []byte + } + inboxes := make([][]pending, len(sessions)) + + for round := 0; round < 50; round++ { + for i, sess := range sessions { + for { + msg, ok, err := sess.NextOutgoing() + if err != nil { + t.Fatalf("party %d NextOutgoing: %v", i, err) + } + if !ok { + break + } + if msg.IsBroadcast() { + for j := range sessions { + if j == i { + continue + } + inboxes[j] = append(inboxes[j], pending{ + sender: uint16(i), broadcast: true, payload: msg.Payload, + }) + } + } else { + inboxes[msg.Recipient] = append(inboxes[msg.Recipient], pending{ + sender: uint16(i), broadcast: false, payload: msg.Payload, + }) + } + } + } + + allDone := true + for _, s := range sessions { + if !s.Done() { + allDone = false + break + } + } + if allDone { + return + } + + any := false + for _, ib := range inboxes { + if len(ib) > 0 { + any = true + break + } + } + if !any { + t.Fatalf("protocol stuck at round %d", round) + } + + for i, ib := range inboxes { + if sessions[i].Done() { + // Session already produced an Output (signature or error). Drop + // remaining queued messages — they're for past rounds. + inboxes[i] = nil + continue + } + for _, p := range ib { + if err := sessions[i].Deliver(p.sender, p.broadcast, p.payload); err != nil { + t.Fatalf("party %d Deliver: %v", i, err) + } + } + inboxes[i] = nil + } + } + t.Fatal("protocol did not finish within 50 rounds") +} + +// Compile-time check that the fixture's ECDSAKeyshare type still has the field +// we reach into. If keygen.LocalPartySaveData ever changes shape, the build +// breaks here. +var _ = keygen.LocalPartySaveData{} + +// Helper: format hex strings deterministically for failure messages. +var _ = hex.EncodeToString +var _ = big.NewInt diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 00000000..ff720706 --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,1121 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cggmp21" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3309aeafc6311b8823faa1ee73e2e776b7fd5a0b3531ee72ec7f1acf40136ffc" +dependencies = [ + "cggmp21-keygen", + "digest", + "futures", + "generic-ec", + "generic-ec-zkp", + "hd-wallet", + "hex", + "key-share", + "paillier-zk", + "rand_core", + "rand_hash", + "round-based", + "serde", + "serde_with 2.3.3", + "sha2", + "thiserror 1.0.69", + "udigest", +] + +[[package]] +name = "cggmp21-ffi" +version = "0.1.0" +dependencies = [ + "cggmp21", + "generic-ec", + "key-share", + "rand_core", + "round-based", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "cggmp21-keygen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa8c850290c494f951abe0350e56c31e4f5664863490197490ff48cb825447d" +dependencies = [ + "digest", + "displaydoc", + "generic-ec", + "generic-ec-zkp", + "hex", + "key-share", + "rand_core", + "round-based", + "serde", + "serde_with 2.3.3", + "sha2", + "thiserror 1.0.69", + "udigest", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "educe" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fast-paillier" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1108d991b54d8e3aa3eb155c07863306cbceafb713ab1ebcef085e19f3cb84c" +dependencies = [ + "bytemuck", + "rand_core", + "rug", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "serde", + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "generic-ec" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de1099ac0b4d87261d67ff5d4ed400af617a1da40b58908d759b9cf5fd8ed27" +dependencies = [ + "curve25519-dalek", + "digest", + "generic-ec-core", + "generic-ec-curves", + "hex", + "phantom-type 0.4.2", + "rand_core", + "rand_hash", + "serde", + "serde_with 2.3.3", + "subtle", + "udigest", + "zeroize", +] + +[[package]] +name = "generic-ec-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcba5fdf70cc3ce5805c487f8523b4ceeb32e8ec5237c71ffd93c1ca47a97fee" +dependencies = [ + "generic-array", + "rand_core", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "generic-ec-curves" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7c6d23001a5eb60eec2b785a63d2ca965fdfbaf3314b3b46df047398369e28" +dependencies = [ + "elliptic-curve", + "generic-ec-core", + "k256", + "rand_core", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "generic-ec-zkp" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd3945c585fdddba3f86bda4e4cfba22d5e255001b3e145c9db305ad096c6d88" +dependencies = [ + "digest", + "generic-array", + "generic-ec", + "rand_core", + "serde", + "subtle", + "udigest", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gmp-mpfr-sys" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7db155b537cb791b133341f99f68371d86ee7fa4c79aacfbc376d72d23c70531" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hd-wallet" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6522551bb35937363845f39a6d4c49e60bdb35a8f7154ebdd078cab50be97992" +dependencies = [ + "generic-array", + "generic-ec", + "hmac", + "sha2", + "subtle", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "elliptic-curve", +] + +[[package]] +name = "key-share" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206b4474f861dedc6fc38e06f7c52c52f1e01180d5284aa62b58844a044fad7d" +dependencies = [ + "displaydoc", + "generic-ec", + "generic-ec-zkp", + "hex", + "rand_core", + "serde", + "serde_with 2.3.3", + "thiserror 1.0.69", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "paillier-zk" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9963224009a2fd339cffd8f6c5c35fc9a91732b89acd4b0ed34c30afe70a193" +dependencies = [ + "digest", + "fast-paillier", + "generic-ec", + "rand_core", + "rand_hash", + "rug", + "serde", + "serde_with 3.20.0", + "thiserror 1.0.69", + "udigest", +] + +[[package]] +name = "phantom-type" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f710afd11c9711b04f97ab61bb9747d5a04562fdf0f9f44abc3de92490084982" +dependencies = [ + "educe", +] + +[[package]] +name = "phantom-type" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68f5dc797c2a743e024e1c53215474598faf0408826a90249569ad7f47adeaa" +dependencies = [ + "educe", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bc1dd921383c6564eb0b8252f5b3f6622b84d40c6e35f5e6790e1fd7abb7a9" +dependencies = [ + "digest", + "rand_core", + "udigest", +] + +[[package]] +name = "round-based" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da76edf50de0a9d6911fc79261bb04cc9f3f3a375e0201799f5edf58499af341" +dependencies = [ + "futures-util", + "phantom-type 0.3.1", + "round-based-derive", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "round-based-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afa4d5b318bcafae8a7ebc57c1cb7d4b2db7358293e34d71bfd605fd327cc13" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rug" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07a8857882aec59d27254b02481c709327c13de6fad1da60bfc4f9783eaaa61e" +dependencies = [ + "az", + "gmp-mpfr-sys", + "libc", + "libm", + "serde", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "base64", + "chrono", + "hex", + "serde", + "serde_json", + "serde_with_macros 2.3.3", + "time", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "serde_core", + "serde_with_macros 3.20.0", +] + +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "udigest" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ff079a60bd5dc98b364ce7b5a633a8937bf558f5d19c9a390f5ae1973cf07e" +dependencies = [ + "digest", + "udigest-derive", +] + +[[package]] +name = "udigest-derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fd5248861b973cd5d1da5604b0ce22a35fa77f015d9f7ed9ab57078205bb86" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 00000000..22cf6883 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["cggmp21-ffi"] diff --git a/rust/cggmp21-ffi/Cargo.toml b/rust/cggmp21-ffi/Cargo.toml new file mode 100644 index 00000000..74f74d22 --- /dev/null +++ b/rust/cggmp21-ffi/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cggmp21-ffi" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "cggmp21_ffi" +crate-type = ["staticlib", "cdylib"] + +[dependencies] +cggmp21 = { version = "0.6.3", features = ["curve-secp256k1", "state-machine", "spof"] } +key-share = { version = "0.6", default-features = false, features = ["serde"] } +generic-ec = { version = "0.4", default-features = false, features = ["curve-secp256k1", "serde", "std"] } +round-based = { version = "0.4.1", default-features = false, features = ["state-machine"] } + +rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +serde = { version = "1", default-features = false, features = ["derive"] } +serde_json = "1" +sha2 = { version = "0.10", default-features = false, features = ["std"] } diff --git a/rust/cggmp21-ffi/examples/gen_fixtures.rs b/rust/cggmp21-ffi/examples/gen_fixtures.rs new file mode 100644 index 00000000..9237eddc --- /dev/null +++ b/rust/cggmp21-ffi/examples/gen_fixtures.rs @@ -0,0 +1,68 @@ +//! Generates 2-of-3 secp256k1 cggmp21 key shares for use as Go test fixtures. +//! +//! Run from the repo root: +//! +//! cargo run --release --manifest-path rust/Cargo.toml \ +//! --example gen_fixtures -- tss/cggmp21/testdata +//! +//! Writes: +//! testdata/share-0.json serialized KeyShare for party 0 +//! testdata/share-1.json serialized KeyShare for party 1 +//! testdata/share-2.json serialized KeyShare for party 2 +//! testdata/pubkey.bin compressed 33-byte secp256k1 shared public key +//! +//! Slow (~10-60s) because it generates Paillier moduli per party. + +use std::{fs, path::PathBuf}; + +use cggmp21::{define_security_level, supported_curves::Secp256k1, trusted_dealer}; +use rand_core::OsRng; + +// Mirrors the CompatLevel defined in src/lib.rs. Must stay in sync; the +// fixtures are deserialised by FFI sessions parameterised on CompatLevel, so +// the levels' SECURITY_BITS must match. +#[derive(Clone)] +pub struct CompatLevel; +define_security_level!(CompatLevel { + security_bits = 256, + epsilon = 230, + ell = 256, + ell_prime = 848, + m = 128, + q = (cggmp21::security_level::_internal::Integer::ONE << 128_u32).into(), +}); + +fn main() { + let outdir: PathBuf = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("tss/cggmp21/testdata")); + fs::create_dir_all(&outdir).expect("create testdata dir"); + + let n: u16 = 3; + let t: u16 = 2; + + eprintln!("Generating {t}-of-{n} secp256k1 key shares — this can take 10-60s…"); + let shares = trusted_dealer::builder::(n) + .set_threshold(Some(t)) + .generate_shares(&mut OsRng) + .expect("generate shares"); + + for (i, share) in shares.iter().enumerate() { + let path = outdir.join(format!("share-{i}.json")); + let json = serde_json::to_vec_pretty(share).expect("serialize share"); + fs::write(&path, &json).expect("write share"); + eprintln!(" {} ({} bytes)", path.display(), json.len()); + } + + let pubkey = shares[0].shared_public_key.to_bytes(true).to_vec(); + let pubkey_path = outdir.join("pubkey.bin"); + fs::write(&pubkey_path, &pubkey).expect("write pubkey"); + eprintln!( + " {} ({} bytes, compressed secp256k1)", + pubkey_path.display(), + pubkey.len() + ); + + eprintln!("Done."); +} diff --git a/rust/cggmp21-ffi/include/cggmp21.h b/rust/cggmp21-ffi/include/cggmp21.h new file mode 100644 index 00000000..f87186a4 --- /dev/null +++ b/rust/cggmp21-ffi/include/cggmp21.h @@ -0,0 +1,303 @@ +/* + * cggmp21.h — C header for the cggmp21-ffi Rust library. + * + * Fixed configuration: secp256k1 curve · SHA-256 digest · SecurityLevel128. + * + * Usage from CGo: + * + * // #cgo LDFLAGS: -L${SRCDIR}/../../rust/lib -lcggmp21_ffi -lm -ldl + * // #include "../../rust/include/cggmp21.h" + * import "C" + */ + +#ifndef CGGMP21_H +#define CGGMP21_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* ── Status codes ──────────────────────────────────────────────────────────── */ + +/** Returned by most functions on success. */ +#define CGGMP21_OK 0 +/** A required pointer argument was NULL or a numeric argument was out of range. */ +#define CGGMP21_ERR_INVALID_ARG 1 +/** JSON serialisation or deserialisation failed. */ +#define CGGMP21_ERR_JSON 2 +/** The MPC protocol returned an error (see cggmp21_last_error). */ +#define CGGMP21_ERR_PROTOCOL 3 +/** The requested operation is only valid after the protocol finishes. */ +#define CGGMP21_ERR_NOT_FINISHED 4 + +/* ── Opaque types ──────────────────────────────────────────────────────────── */ + +/** + * Opaque handle for a single-party signing session. + * Must be freed with cggmp21_signing_free when no longer needed. + */ +typedef struct CggmpSigningSession CggmpSigningSession; + +/* ── Session lifecycle ─────────────────────────────────────────────────────── */ + +/** + * Create a new signing session for this party. + * + * @param key_share_json JSON-serialised KeyShare (from cggmp21 keygen + aux-info). + * @param key_share_len Length of key_share_json in bytes. + * @param eid Execution-ID bytes. Must be unique per protocol execution. + * @param eid_len Length of eid in bytes. + * @param i This party's signing index (0-based, must be < parties_count). + * @param parties_indexes Array of length parties_count. parties_indexes[j] is the keygen + * index of the j-th signer participating in this round. + * @param parties_count Number of signing parties (= threshold t). + * @param data_hash 32-byte pre-hashed digest to sign (e.g. Keccak-256 or SHA-256 + * output produced by the caller). The library signs this value + * as-is, reduced modulo the secp256k1 curve order. It does NOT + * hash the input again. + * @param data_hash_len Must be 32. + * @param out On CGGMP21_OK, written with the new session pointer. + * + * @return CGGMP21_OK on success, otherwise an error code; call cggmp21_last_error() for details. + */ +int cggmp21_signing_new( + const uint8_t *key_share_json, size_t key_share_len, + const uint8_t *eid, size_t eid_len, + uint16_t i, + const uint16_t *parties_indexes, size_t parties_count, + const uint8_t *data_hash, size_t data_hash_len, + CggmpSigningSession **out +); + +/** + * Free a session created by cggmp21_signing_new. + * Passing NULL is a no-op. + */ +void cggmp21_signing_free(CggmpSigningSession *session); + +/* ── Protocol stepping ─────────────────────────────────────────────────────── */ + +/** + * Deliver an incoming protocol message from another party. + * + * After each call, drain any queued outgoing messages with + * cggmp21_signing_poll_outgoing before delivering the next message. + * + * @param session The session that receives the message. + * @param sender Sender's signing index (0-based). + * @param is_broadcast Non-zero if the message is a broadcast; zero for point-to-point. + * @param msg_json JSON-serialised protocol message bytes. + * @param msg_len Length of msg_json in bytes. + * + * @return CGGMP21_OK, CGGMP21_ERR_JSON, or CGGMP21_ERR_PROTOCOL. + */ +int cggmp21_signing_deliver( + CggmpSigningSession *session, + uint16_t sender, + uint8_t is_broadcast, + const uint8_t *msg_json, size_t msg_len +); + +/** + * Returns non-zero if the protocol has finished (successfully or with an error). + * Once this returns non-zero, call cggmp21_signing_result to get the signature. + */ +int cggmp21_signing_is_done(const CggmpSigningSession *session); + +/* ── Outgoing messages ─────────────────────────────────────────────────────── */ + +/** + * Pop the next pending outgoing protocol message. + * + * Returns 1 if a message was available; 0 if the queue is empty. + * Call in a loop until it returns 0 to drain all queued messages. + * + * On success (return value 1): + * - *out_recipient is set to -1 for a broadcast message, or to the target + * party's signing index for a point-to-point message. + * - *out_json points to a heap-allocated buffer of *out_len bytes containing + * the JSON-serialised protocol message. The caller MUST free it with + * cggmp21_free_buffer(*out_json, *out_len). + * + * @param session The session to poll. + * @param out_recipient Written with the recipient index or -1. + * @param out_json Written with a pointer to the message buffer. + * @param out_len Written with the length of the message buffer. + */ +int cggmp21_signing_poll_outgoing( + CggmpSigningSession *session, + int32_t *out_recipient, + uint8_t **out_json, + size_t *out_len +); + +/* ── Result retrieval ──────────────────────────────────────────────────────── */ + +/** + * Retrieve the signature after the protocol finishes successfully. + * + * The signature is written as r || s in big-endian encoding. + * Use cggmp21_signature_len() to obtain the required buffer size. + * + * @param session A finished session (cggmp21_signing_is_done returns non-zero). + * @param out_sig Buffer of at least cggmp21_signature_len() bytes. + * @param out_sig_len Length of out_sig in bytes. + * + * @return CGGMP21_OK on success. + * CGGMP21_ERR_NOT_FINISHED if the protocol is not yet complete. + * CGGMP21_ERR_PROTOCOL if the protocol completed with an error. + * CGGMP21_ERR_INVALID_ARG if out_sig_len is too small. + */ +int cggmp21_signing_result( + const CggmpSigningSession *session, + uint8_t *out_sig, size_t out_sig_len +); + +/** + * Returns the byte length of a serialised secp256k1 signature (r || s). + * Typically 64 bytes. + */ +size_t cggmp21_signature_len(void); + +/* ── Utilities ─────────────────────────────────────────────────────────────── */ + +/** + * Free a buffer that was allocated by a cggmp21 FFI function. + * Passing NULL or len=0 is a no-op. + * + * @param buf Pointer returned in an out-parameter by a cggmp21 function. + * @param len Byte length reported alongside the pointer. + */ +void cggmp21_free_buffer(uint8_t *buf, size_t len); + +/** + * Returns a pointer to a null-terminated string describing the most recent + * error on this thread. The pointer is valid until the next cggmp21 call on + * this thread. Returns an empty string if no error has occurred. + */ +const char *cggmp21_last_error(void); + +/** + * Returns the cggmp21-ffi library version as a null-terminated string. + */ +const char *cggmp21_version(void); + +/* ── One-round signing: presignature + partial sign + combine ─────────────── */ + +/** + * Opaque handle for a presignature-generation MPC session. + * Must be freed with cggmp21_presign_free. + */ +typedef struct CggmpPresignSession CggmpPresignSession; + +/** + * Start a presignature-generation session. + * + * Drive the protocol with cggmp21_presign_deliver / cggmp21_presign_poll_outgoing, + * just like a signing session. The presignature is message-independent — no + * data_hash is required at this stage. + * + * Once cggmp21_presign_is_done returns non-zero, fetch the serialized + * Presignature with cggmp21_presign_result. + * + * Parameters mirror cggmp21_signing_new minus data_hash. + * + * @return CGGMP21_OK on success; otherwise call cggmp21_last_error(). + */ +int cggmp21_presign_new( + const uint8_t *key_share_json, size_t key_share_len, + const uint8_t *eid, size_t eid_len, + uint16_t i, + const uint16_t *parties_indexes, size_t parties_count, + CggmpPresignSession **out +); + +/** Free a presignature session. NULL is a no-op. */ +void cggmp21_presign_free(CggmpPresignSession *session); + +/** Deliver an incoming protocol message to a presignature session. */ +int cggmp21_presign_deliver( + CggmpPresignSession *session, + uint16_t sender, + uint8_t is_broadcast, + const uint8_t *msg_json, size_t msg_len +); + +/** + * Pop the next outgoing message from a presignature session. + * Same contract as cggmp21_signing_poll_outgoing: returns 1 if a message was + * available, 0 otherwise; *out_json must be freed with cggmp21_free_buffer. + */ +int cggmp21_presign_poll_outgoing( + CggmpPresignSession *session, + int32_t *out_recipient, + uint8_t **out_json, + size_t *out_len +); + +/** Returns non-zero once the presignature protocol has finished. */ +int cggmp21_presign_is_done(const CggmpPresignSession *session); + +/** + * Retrieve the JSON-serialised Presignature once the protocol finishes. + * + * On success, *out_json is a heap-allocated buffer that the caller MUST free + * with cggmp21_free_buffer(*out_json, *out_len). + * + * @return CGGMP21_OK, CGGMP21_ERR_NOT_FINISHED, or CGGMP21_ERR_PROTOCOL. + */ +int cggmp21_presign_result( + const CggmpPresignSession *session, + uint8_t **out_json, size_t *out_len +); + +/** + * Locally compute a PartialSignature from a presignature and a 32-byte hash. + * + * **Never reuse a presignature** — signing two different messages with the + * same presignature leaks the private key. + * + * @param presignature_json JSON-serialised Presignature (from cggmp21_presign_result). + * @param data_hash 32-byte pre-hashed digest (e.g. Keccak-256 or SHA-256 output). + * Signed as-is mod curve order; the library does NOT re-hash. + * @param out_json Receives a heap-allocated JSON PartialSignature buffer. + * @param out_len Receives the buffer length. + * + * On success, free *out_json with cggmp21_free_buffer(*out_json, *out_len). + * + * @return CGGMP21_OK, CGGMP21_ERR_INVALID_ARG, or CGGMP21_ERR_JSON. + */ +int cggmp21_partial_sign( + const uint8_t *presignature_json, size_t presignature_len, + const uint8_t *data_hash, size_t data_hash_len, + uint8_t **out_json, size_t *out_len +); + +/** + * Combine a JSON array of PartialSignatures into a final ECDSA signature. + * + * @param partials_json_array JSON array bytes: [partial_sig_1, partial_sig_2, ...]. + * Must contain >= threshold partials, all derived from + * the same presignature run. + * @param out_sig Buffer of at least cggmp21_signature_len() bytes. + * @param out_sig_len Length of out_sig in bytes. + * + * The signature is written as r || s, big-endian. May produce an invalid + * signature if some signer cheated — verify before trusting. + * + * @return CGGMP21_OK, CGGMP21_ERR_INVALID_ARG, CGGMP21_ERR_JSON, + * or CGGMP21_ERR_PROTOCOL (empty/malformed input). + */ +int cggmp21_combine_partials( + const uint8_t *partials_json_array, size_t partials_len, + uint8_t *out_sig, size_t out_sig_len +); + +#ifdef __cplusplus +} +#endif + +#endif /* CGGMP21_H */ diff --git a/rust/cggmp21-ffi/src/lib.rs b/rust/cggmp21-ffi/src/lib.rs new file mode 100644 index 00000000..ce3bd457 --- /dev/null +++ b/rust/cggmp21-ffi/src/lib.rs @@ -0,0 +1,828 @@ +//! CGo-compatible FFI for cggmp21 threshold signing. +//! +//! All functions are safe to call from C/CGo. Opaque session pointers must be +//! freed with the corresponding `_free` function. Buffers returned via out-params +//! must be freed with `cggmp21_free_buffer`. +//! +//! Fixed configuration: secp256k1 curve, SHA-256 digest, SecurityLevel128. +//! +//! ## Protocol loop (from the Go side) +//! +//! ```text +//! 1. session = cggmp21_signing_new(...) +//! 2. loop: +//! // drain any initial outgoing messages +//! while cggmp21_signing_poll_outgoing(...) == 1: +//! send message to recipient +//! if cggmp21_signing_is_done(session): break +//! // receive one message from the network +//! msg = recv_from_network() +//! cggmp21_signing_deliver(session, msg.sender, msg.is_broadcast, msg.data) +//! 3. cggmp21_signing_result(session, sig_buf, sig_buf_len) +//! 4. cggmp21_signing_free(session) +//! ``` + +#![allow(clippy::missing_safety_doc)] + +use std::collections::VecDeque; +use std::mem::ManuallyDrop; +use std::os::raw::c_char; + +use cggmp21::{ + signing::msg::Msg, supported_curves::Secp256k1, DataToSign, ExecutionId, KeyShare, + PartialSignature, Presignature, Signature, SigningError, +}; +use cggmp21::define_security_level; +use cggmp21::generic_ec::Scalar; +use rand_core::OsRng; +use round_based::{ + state_machine::{ProceedResult, StateMachine}, + Incoming, MessageDestination, MessageType, +}; +use sha2::Sha256; + +// CompatLevel — a relaxed cggmp21 security level matching legacy tss-lib +// keyshares (1024-bit Paillier primes, 2048-bit modulus). The CGGMP21 protocol +// constants (epsilon/ell/ell_prime/m/q) are kept at cggmp21's stock +// SecurityLevel128 values, which oversizes the ZK proofs but does NOT weaken +// soundness — they just give more margin than needed at this prime size. +// SECURITY_BITS=256 is the only parameter that gates the Paillier prime size +// check (4·SECURITY_BITS ≤ prime bits). Net effect: ~112-bit symmetric- +// equivalent security from the Paillier modulus, vs cggmp21's intended ~128. +// Documented tradeoff for the tss-lib → cggmp21 migration. +#[derive(Clone)] +pub struct CompatLevel; +define_security_level!(CompatLevel { + security_bits = 256, + epsilon = 230, + ell = 256, + ell_prime = 848, + m = 128, + q = (cggmp21::security_level::_internal::Integer::ONE << 128_u32).into(), +}); + +type E = Secp256k1; +type D = Sha256; +type L = CompatLevel; +type SigningMsg = Msg; +type SigningOutput = Result, SigningError>; +type PresignOutput = Result, SigningError>; +type SigningSM = dyn StateMachine; +type PresignSM = dyn StateMachine; + +// ── Status codes ────────────────────────────────────────────────────────────── + +pub const CGGMP21_OK: i32 = 0; +pub const CGGMP21_ERR_INVALID_ARG: i32 = 1; +pub const CGGMP21_ERR_JSON: i32 = 2; +pub const CGGMP21_ERR_PROTOCOL: i32 = 3; +pub const CGGMP21_ERR_NOT_FINISHED: i32 = 4; + +// ── Thread-local error storage ──────────────────────────────────────────────── + +std::thread_local! { + static LAST_ERROR: std::cell::RefCell = const { std::cell::RefCell::new(String::new()) }; +} + +fn set_last_error(msg: impl Into) { + LAST_ERROR.with(|e| *e.borrow_mut() = msg.into()); +} + +/// Format an error chain (top-level message + every `source()` in turn) so the +/// FFI surfaces the underlying cause instead of just "signing protocol failed". +fn format_err_chain(err: &dyn std::error::Error) -> String { + let mut out = err.to_string(); + let mut src = err.source(); + while let Some(e) = src { + out.push_str(": "); + out.push_str(&e.to_string()); + src = e.source(); + } + out +} + +// ── Signing session ─────────────────────────────────────────────────────────── + +/// Opaque handle for a signing protocol session. +/// +/// Each party participating in the signing protocol holds one session. +/// Drive the protocol by alternating `deliver` and `poll_outgoing` calls. +pub struct CggmpSigningSession { + // Leaked boxes – freed in Drop after `sm` is dropped first. + lk_key_share: *mut KeyShare, + lk_eid: *mut [u8], + lk_parties: *mut [u16], + lk_rng: *mut OsRng, + + // State machine holding 'static references into the leaked boxes. + // SAFETY invariant: sm is dropped before the leaked boxes are freed (see Drop impl). + sm: ManuallyDrop>, + + // Outgoing messages pending delivery, as (recipient, json_bytes). + // recipient == -1 means broadcast; ≥ 0 is the target party's signing index. + outgoing: VecDeque<(i32, Vec)>, + + // Filled once the protocol finishes. + result: Option, String>>, + + // Monotonically-increasing ID fed to round-based Incoming. + msg_id: u64, +} + +impl Drop for CggmpSigningSession { + fn drop(&mut self) { + unsafe { + // Drop the state machine first – it holds 'static refs into the boxes below. + ManuallyDrop::drop(&mut self.sm); + drop(Box::from_raw(self.lk_key_share)); + drop(Box::from_raw(self.lk_eid)); + drop(Box::from_raw(self.lk_parties)); + drop(Box::from_raw(self.lk_rng)); + } + } +} + +/// Drive `sm.proceed()` until it blocks on a message, finishes, or errors. +/// Outgoing messages are collected into `outgoing`; the final result into `result`. +fn drive( + sm: &mut Box, + outgoing: &mut VecDeque<(i32, Vec)>, + result: &mut Option, String>>, +) -> i32 { + loop { + match sm.proceed() { + ProceedResult::SendMsg(msg) => { + let recipient = match msg.recipient { + MessageDestination::AllParties => -1i32, + MessageDestination::OneParty(idx) => idx as i32, + }; + match serde_json::to_vec(&msg.msg) { + Ok(bytes) => outgoing.push_back((recipient, bytes)), + Err(e) => { + set_last_error(format!("outgoing serialize: {e}")); + return CGGMP21_ERR_JSON; + } + } + } + ProceedResult::Yielded => { + // Protocol voluntarily paused; resume immediately. + } + ProceedResult::NeedsOneMoreMessage => { + // Waiting for an incoming message – hand control back to the caller. + return CGGMP21_OK; + } + ProceedResult::Output(output) => { + match output { + Ok(sig) => { + let len = Signature::::serialized_len(); + let mut buf = vec![0u8; len]; + sig.write_to_slice(&mut buf); + *result = Some(Ok(buf)); + } + Err(e) => { + *result = Some(Err(format_err_chain(&e))); + } + } + return CGGMP21_OK; + } + ProceedResult::Error(e) => { + set_last_error(format!("state machine error: {e:?}")); + return CGGMP21_ERR_PROTOCOL; + } + } + } +} + +// ── Presignature session (mirrors the signing one, with Presignature output) ── + +/// Opaque handle for an MPC presignature-generation session. +/// +/// Use this for the offline phase of the one-round signing flow: +/// 1. `cggmp21_presign_new` → drive with deliver/poll until `is_done`. +/// 2. `cggmp21_presign_result` → serialized [`Presignature`] JSON. +/// 3. Later: `cggmp21_partial_sign(presig_json, msg_hash, …)` → `PartialSignature` JSON. +/// 4. `cggmp21_combine_partials(partials_json_array, …)` → 64-byte ECDSA signature. +pub struct CggmpPresignSession { + lk_key_share: *mut KeyShare, + lk_eid: *mut [u8], + lk_parties: *mut [u16], + lk_rng: *mut OsRng, + sm: ManuallyDrop>, + outgoing: VecDeque<(i32, Vec)>, + result: Option, String>>, + msg_id: u64, +} + +impl Drop for CggmpPresignSession { + fn drop(&mut self) { + unsafe { + ManuallyDrop::drop(&mut self.sm); + drop(Box::from_raw(self.lk_key_share)); + drop(Box::from_raw(self.lk_eid)); + drop(Box::from_raw(self.lk_parties)); + drop(Box::from_raw(self.lk_rng)); + } + } +} + +/// Drive the presignature state machine. Mirrors [`drive`] but serializes the +/// final [`Presignature`] to JSON instead of writing raw signature bytes. +fn drive_presign( + sm: &mut Box, + outgoing: &mut VecDeque<(i32, Vec)>, + result: &mut Option, String>>, +) -> i32 { + loop { + match sm.proceed() { + ProceedResult::SendMsg(msg) => { + let recipient = match msg.recipient { + MessageDestination::AllParties => -1i32, + MessageDestination::OneParty(idx) => idx as i32, + }; + match serde_json::to_vec(&msg.msg) { + Ok(bytes) => outgoing.push_back((recipient, bytes)), + Err(e) => { + set_last_error(format!("outgoing serialize: {e}")); + return CGGMP21_ERR_JSON; + } + } + } + ProceedResult::Yielded => {} + ProceedResult::NeedsOneMoreMessage => return CGGMP21_OK, + ProceedResult::Output(output) => { + match output { + Ok(presig) => match serde_json::to_vec(&presig) { + Ok(bytes) => *result = Some(Ok(bytes)), + Err(e) => { + set_last_error(format!("presignature serialize: {e}")); + return CGGMP21_ERR_JSON; + } + }, + Err(e) => { + *result = Some(Err(format_err_chain(&e))); + } + } + return CGGMP21_OK; + } + ProceedResult::Error(e) => { + set_last_error(format!("state machine error: {e:?}")); + return CGGMP21_ERR_PROTOCOL; + } + } + } +} + +// ── Constructor / destructor ────────────────────────────────────────────────── + +/// Create a new signing session. +/// +/// # Parameters +/// - `key_share_json` / `key_share_len`: JSON-serialized `KeyShare`. +/// - `eid` / `eid_len`: Execution ID bytes (must be unique per protocol run). +/// - `i`: This party's signing index (0-based). +/// - `parties_indexes` / `parties_count`: `parties_indexes[j]` is the keygen +/// index of the j-th signer participating in this round. +/// - `data_hash` / `data_hash_len`: 32-byte big-endian hash of the message to sign. +/// - `out`: On success, written with a pointer to the new session. +/// +/// Returns `CGGMP21_OK` on success; sets `cggmp21_last_error()` on failure. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_signing_new( + key_share_json: *const u8, + key_share_len: usize, + eid: *const u8, + eid_len: usize, + i: u16, + parties_indexes: *const u16, + parties_count: usize, + data_hash: *const u8, + data_hash_len: usize, + out: *mut *mut CggmpSigningSession, +) -> i32 { + if key_share_json.is_null() + || eid.is_null() + || parties_indexes.is_null() + || data_hash.is_null() + || out.is_null() + { + set_last_error("null pointer argument"); + return CGGMP21_ERR_INVALID_ARG; + } + + let ks_bytes = std::slice::from_raw_parts(key_share_json, key_share_len); + let key_share: KeyShare = match serde_json::from_slice(ks_bytes) { + Ok(ks) => ks, + Err(e) => { + set_last_error(format!("key_share deserialize: {e}")); + return CGGMP21_ERR_JSON; + } + }; + + let hash_bytes = std::slice::from_raw_parts(data_hash, data_hash_len); + if hash_bytes.len() != 32 { + set_last_error(format!( + "data_hash must be 32 bytes, got {}", + hash_bytes.len() + )); + return CGGMP21_ERR_INVALID_ARG; + } + + // The 32-byte input is treated as the final pre-hash digest (e.g. Keccak-256 + // or SHA-256 output produced by the caller) and signed as-is, modulo the + // curve order. The FFI does NOT hash it again. + let scalar = Scalar::::from_be_bytes_mod_order(hash_bytes); + let data_to_sign = DataToSign::::from_scalar(scalar); + + let eid_vec: Vec = std::slice::from_raw_parts(eid, eid_len).to_vec(); + let parties_vec: Vec = + std::slice::from_raw_parts(parties_indexes, parties_count).to_vec(); + + // Leak inputs that need 'static references for the state machine. + let lk_key_share: &'static KeyShare = &*Box::into_raw(Box::new(key_share)); + let lk_eid: &'static [u8] = Box::leak(eid_vec.into_boxed_slice()); + let lk_parties: &'static [u16] = Box::leak(parties_vec.into_boxed_slice()); + let rng_ptr: *mut OsRng = Box::into_raw(Box::new(OsRng)); + let lk_rng: &'static mut OsRng = &mut *rng_ptr; + + let sm_impl = cggmp21::signing(ExecutionId::new(lk_eid), i, lk_parties, lk_key_share) + .sign_sync(lk_rng, data_to_sign); + + let sm: Box = Box::new(sm_impl); + + let mut session = Box::new(CggmpSigningSession { + lk_key_share: lk_key_share as *const _ as *mut _, + lk_eid: lk_eid as *const _ as *mut _, + lk_parties: lk_parties as *const _ as *mut _, + lk_rng: rng_ptr, + sm: ManuallyDrop::new(sm), + outgoing: VecDeque::new(), + result: None, + msg_id: 0, + }); + + // Advance until the protocol asks for its first incoming message. + let rc = drive(&mut session.sm, &mut session.outgoing, &mut session.result); + if rc != CGGMP21_OK { + // drive already called set_last_error; let Drop clean up the session + return rc; + } + + *out = Box::into_raw(session); + CGGMP21_OK +} + +/// Free a signing session. Passing NULL is a no-op. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_signing_free(session: *mut CggmpSigningSession) { + if !session.is_null() { + drop(Box::from_raw(session)); + } +} + +// ── Protocol stepping ───────────────────────────────────────────────────────── + +/// Deliver an incoming protocol message from another party. +/// +/// Call `cggmp21_signing_poll_outgoing` in a loop after each deliver to drain +/// any outgoing messages before delivering the next one. +/// +/// - `sender`: Sender's signing index (0-based). +/// - `is_broadcast`: Non-zero if broadcast; zero for point-to-point. +/// - `msg_json` / `msg_len`: JSON-serialized protocol message. +/// +/// Returns `CGGMP21_OK`, `CGGMP21_ERR_JSON`, or `CGGMP21_ERR_PROTOCOL`. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_signing_deliver( + session: *mut CggmpSigningSession, + sender: u16, + is_broadcast: u8, + msg_json: *const u8, + msg_len: usize, +) -> i32 { + let s = &mut *session; + + let bytes = std::slice::from_raw_parts(msg_json, msg_len); + let msg: SigningMsg = match serde_json::from_slice(bytes) { + Ok(m) => m, + Err(e) => { + set_last_error(format!("msg deserialize: {e}")); + return CGGMP21_ERR_JSON; + } + }; + + let msg_type = if is_broadcast != 0 { + MessageType::Broadcast + } else { + MessageType::P2P + }; + + s.msg_id += 1; + let incoming = Incoming { + id: s.msg_id, + sender, + msg_type, + msg, + }; + + // received_msg returns Err(msg) if the message is rejected (wrong round / unexpected). + if let Err(_rejected) = s.sm.received_msg(incoming) { + set_last_error("message rejected by state machine (unexpected sender or round)"); + return CGGMP21_ERR_PROTOCOL; + } + + drive(&mut s.sm, &mut s.outgoing, &mut s.result) +} + +/// Returns non-zero if the protocol has finished (successfully or with an error). +#[no_mangle] +pub unsafe extern "C" fn cggmp21_signing_is_done(session: *const CggmpSigningSession) -> i32 { + if (*session).result.is_some() { 1 } else { 0 } +} + +// ── Outgoing messages ───────────────────────────────────────────────────────── + +/// Pop the next pending outgoing protocol message. +/// +/// Returns 1 if a message is available; 0 when the queue is empty. +/// Call repeatedly after each `cggmp21_signing_new` or `cggmp21_signing_deliver` +/// to drain all pending messages before waiting for the next incoming one. +/// +/// On return value 1: +/// - `*out_recipient` is -1 for broadcast, or the target party's signing index. +/// - `*out_json` / `*out_len` hold the JSON message; **must** be freed with +/// `cggmp21_free_buffer(*out_json, *out_len)`. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_signing_poll_outgoing( + session: *mut CggmpSigningSession, + out_recipient: *mut i32, + out_json: *mut *mut u8, + out_len: *mut usize, +) -> i32 { + let s = &mut *session; + match s.outgoing.pop_front() { + None => 0, + Some((recipient, bytes)) => { + *out_recipient = recipient; + *out_len = bytes.len(); + let mut boxed = bytes.into_boxed_slice(); + *out_json = boxed.as_mut_ptr(); + std::mem::forget(boxed); + 1 + } + } +} + +// ── Result retrieval ────────────────────────────────────────────────────────── + +/// Retrieve the signature once the protocol completes successfully. +/// +/// Writes `r || s` (big-endian) into `out_sig`. Use `cggmp21_signature_len()` +/// to get the required buffer size (64 bytes for secp256k1). +/// +/// Returns `CGGMP21_ERR_NOT_FINISHED` if the protocol is still running, +/// `CGGMP21_ERR_PROTOCOL` if it failed (see `cggmp21_last_error`). +#[no_mangle] +pub unsafe extern "C" fn cggmp21_signing_result( + session: *const CggmpSigningSession, + out_sig: *mut u8, + out_sig_len: usize, +) -> i32 { + let s = &*session; + match &s.result { + None => { + set_last_error("protocol not finished yet"); + CGGMP21_ERR_NOT_FINISHED + } + Some(Err(e)) => { + set_last_error(e.clone()); + CGGMP21_ERR_PROTOCOL + } + Some(Ok(sig_bytes)) => { + let expected = Signature::::serialized_len(); + if out_sig_len < expected { + set_last_error(format!( + "output buffer too small: need {expected}, got {out_sig_len}" + )); + return CGGMP21_ERR_INVALID_ARG; + } + std::ptr::copy_nonoverlapping(sig_bytes.as_ptr(), out_sig, sig_bytes.len()); + CGGMP21_OK + } + } +} + +/// Byte length of a serialized secp256k1 signature (`r || s`). Typically 64. +#[no_mangle] +pub extern "C" fn cggmp21_signature_len() -> usize { + Signature::::serialized_len() +} + +// ── Utilities ───────────────────────────────────────────────────────────────── + +/// Free a buffer allocated by a cggmp21 FFI function. NULL / len=0 is a no-op. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_free_buffer(buf: *mut u8, len: usize) { + if !buf.is_null() && len > 0 { + drop(Vec::from_raw_parts(buf, len, len)); + } +} + +/// Returns a null-terminated string describing the most recent error on this +/// thread. Valid until the next cggmp21 call on this thread. +#[no_mangle] +pub extern "C" fn cggmp21_last_error() -> *const c_char { + LAST_ERROR.with(|e| { + let s = e.borrow(); + let c_str = std::ffi::CString::new(s.as_str()).unwrap_or_default(); + c_str.into_raw() as *const c_char + }) +} + +/// cggmp21-ffi version string (null-terminated). +#[no_mangle] +pub extern "C" fn cggmp21_version() -> *const c_char { + concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char +} + +// ───────────────────────────────────────────────────────────────────────────── +// One-round signing flow +// ---------------------- +// Three pieces: +// 1. Presignature MPC session (multi-round, message-independent) +// 2. Local partial signature (single party, given presig + message) +// 3. Combine partial signatures (one broadcast round → final ECDSA sig) +// ───────────────────────────────────────────────────────────────────────────── + +/// Allocate a heap buffer (Box<[u8]>) into the FFI out-pointer pair. +/// The caller must free it with `cggmp21_free_buffer`. +unsafe fn alloc_out_buffer(bytes: Vec, out_json: *mut *mut u8, out_len: *mut usize) { + *out_len = bytes.len(); + let mut boxed = bytes.into_boxed_slice(); + *out_json = boxed.as_mut_ptr(); + std::mem::forget(boxed); +} + +/// Create a new presignature-generation session. +/// +/// Inputs match `cggmp21_signing_new` except there is no message hash: +/// presignatures are message-independent. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_presign_new( + key_share_json: *const u8, + key_share_len: usize, + eid: *const u8, + eid_len: usize, + i: u16, + parties_indexes: *const u16, + parties_count: usize, + out: *mut *mut CggmpPresignSession, +) -> i32 { + if key_share_json.is_null() || eid.is_null() || parties_indexes.is_null() || out.is_null() { + set_last_error("null pointer argument"); + return CGGMP21_ERR_INVALID_ARG; + } + + let ks_bytes = std::slice::from_raw_parts(key_share_json, key_share_len); + let key_share: KeyShare = match serde_json::from_slice(ks_bytes) { + Ok(ks) => ks, + Err(e) => { + set_last_error(format!("key_share deserialize: {e}")); + return CGGMP21_ERR_JSON; + } + }; + + let eid_vec: Vec = std::slice::from_raw_parts(eid, eid_len).to_vec(); + let parties_vec: Vec = + std::slice::from_raw_parts(parties_indexes, parties_count).to_vec(); + + let lk_key_share: &'static KeyShare = &*Box::into_raw(Box::new(key_share)); + let lk_eid: &'static [u8] = Box::leak(eid_vec.into_boxed_slice()); + let lk_parties: &'static [u16] = Box::leak(parties_vec.into_boxed_slice()); + let rng_ptr: *mut OsRng = Box::into_raw(Box::new(OsRng)); + let lk_rng: &'static mut OsRng = &mut *rng_ptr; + + let sm_impl = cggmp21::signing(ExecutionId::new(lk_eid), i, lk_parties, lk_key_share) + .generate_presignature_sync(lk_rng); + let sm: Box = Box::new(sm_impl); + + let mut session = Box::new(CggmpPresignSession { + lk_key_share: lk_key_share as *const _ as *mut _, + lk_eid: lk_eid as *const _ as *mut _, + lk_parties: lk_parties as *const _ as *mut _, + lk_rng: rng_ptr, + sm: ManuallyDrop::new(sm), + outgoing: VecDeque::new(), + result: None, + msg_id: 0, + }); + + let rc = drive_presign(&mut session.sm, &mut session.outgoing, &mut session.result); + if rc != CGGMP21_OK { + return rc; + } + + *out = Box::into_raw(session); + CGGMP21_OK +} + +/// Free a presignature session. Passing NULL is a no-op. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_presign_free(session: *mut CggmpPresignSession) { + if !session.is_null() { + drop(Box::from_raw(session)); + } +} + +/// Deliver an incoming message to a presignature session. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_presign_deliver( + session: *mut CggmpPresignSession, + sender: u16, + is_broadcast: u8, + msg_json: *const u8, + msg_len: usize, +) -> i32 { + let s = &mut *session; + let bytes = std::slice::from_raw_parts(msg_json, msg_len); + let msg: SigningMsg = match serde_json::from_slice(bytes) { + Ok(m) => m, + Err(e) => { + set_last_error(format!("msg deserialize: {e}")); + return CGGMP21_ERR_JSON; + } + }; + let msg_type = if is_broadcast != 0 { + MessageType::Broadcast + } else { + MessageType::P2P + }; + s.msg_id += 1; + let incoming = Incoming { + id: s.msg_id, + sender, + msg_type, + msg, + }; + if s.sm.received_msg(incoming).is_err() { + set_last_error("message rejected by state machine (unexpected sender or round)"); + return CGGMP21_ERR_PROTOCOL; + } + drive_presign(&mut s.sm, &mut s.outgoing, &mut s.result) +} + +/// Pop the next outgoing message from a presignature session. Same contract as +/// `cggmp21_signing_poll_outgoing`. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_presign_poll_outgoing( + session: *mut CggmpPresignSession, + out_recipient: *mut i32, + out_json: *mut *mut u8, + out_len: *mut usize, +) -> i32 { + let s = &mut *session; + match s.outgoing.pop_front() { + None => 0, + Some((recipient, bytes)) => { + *out_recipient = recipient; + alloc_out_buffer(bytes, out_json, out_len); + 1 + } + } +} + +/// Returns non-zero once the presignature protocol has finished. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_presign_is_done(session: *const CggmpPresignSession) -> i32 { + if (*session).result.is_some() { 1 } else { 0 } +} + +/// Retrieve the JSON-serialized [`Presignature`] after the protocol completes. +/// +/// On success the caller MUST free `*out_json` with `cggmp21_free_buffer`. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_presign_result( + session: *const CggmpPresignSession, + out_json: *mut *mut u8, + out_len: *mut usize, +) -> i32 { + let s = &*session; + match &s.result { + None => { + set_last_error("presignature not finished yet"); + CGGMP21_ERR_NOT_FINISHED + } + Some(Err(e)) => { + set_last_error(e.clone()); + CGGMP21_ERR_PROTOCOL + } + Some(Ok(bytes)) => { + alloc_out_buffer(bytes.clone(), out_json, out_len); + CGGMP21_OK + } + } +} + +/// Locally compute a [`PartialSignature`] from a presignature and a 32-byte +/// message hash. **A presignature must NEVER be reused** — doing so leaks the +/// private key. +/// +/// Output is JSON; caller frees `*out_json` with `cggmp21_free_buffer`. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_partial_sign( + presignature_json: *const u8, + presignature_len: usize, + data_hash: *const u8, + data_hash_len: usize, + out_json: *mut *mut u8, + out_len: *mut usize, +) -> i32 { + if presignature_json.is_null() || data_hash.is_null() || out_json.is_null() || out_len.is_null() + { + set_last_error("null pointer argument"); + return CGGMP21_ERR_INVALID_ARG; + } + + let presig_bytes = std::slice::from_raw_parts(presignature_json, presignature_len); + let presig: Presignature = match serde_json::from_slice(presig_bytes) { + Ok(p) => p, + Err(e) => { + set_last_error(format!("presignature deserialize: {e}")); + return CGGMP21_ERR_JSON; + } + }; + + let hash_bytes = std::slice::from_raw_parts(data_hash, data_hash_len); + if hash_bytes.len() != 32 { + set_last_error(format!( + "data_hash must be 32 bytes, got {}", + hash_bytes.len() + )); + return CGGMP21_ERR_INVALID_ARG; + } + let scalar = Scalar::::from_be_bytes_mod_order(hash_bytes); + let data_to_sign = DataToSign::::from_scalar(scalar); + + let partial = presig.issue_partial_signature(data_to_sign); + match serde_json::to_vec(&partial) { + Ok(bytes) => { + alloc_out_buffer(bytes, out_json, out_len); + CGGMP21_OK + } + Err(e) => { + set_last_error(format!("partial signature serialize: {e}")); + CGGMP21_ERR_JSON + } + } +} + +/// Combine a threshold-sized JSON array of [`PartialSignature`]s into a final +/// ECDSA signature. Writes `r || s` (big-endian, 64 bytes on secp256k1) to +/// `out_sig`. Use `cggmp21_signature_len()` for the required size. +/// +/// `partials_json_array` must deserialize to `[PartialSignature, …]`. +/// +/// Returns `CGGMP21_ERR_PROTOCOL` if `combine` rejects the input (e.g. mixed +/// presignatures, malformed scalars, or empty array). The returned signature +/// may still be invalid for the public key/message if a signer cheated — +/// callers should verify before trusting it. +#[no_mangle] +pub unsafe extern "C" fn cggmp21_combine_partials( + partials_json_array: *const u8, + partials_len: usize, + out_sig: *mut u8, + out_sig_len: usize, +) -> i32 { + if partials_json_array.is_null() || out_sig.is_null() { + set_last_error("null pointer argument"); + return CGGMP21_ERR_INVALID_ARG; + } + + let bytes = std::slice::from_raw_parts(partials_json_array, partials_len); + let partials: Vec> = match serde_json::from_slice(bytes) { + Ok(v) => v, + Err(e) => { + set_last_error(format!("partials deserialize: {e}")); + return CGGMP21_ERR_JSON; + } + }; + + let sig = match PartialSignature::::combine(&partials) { + Some(s) => s, + None => { + set_last_error("combine rejected partials (empty or malformed)"); + return CGGMP21_ERR_PROTOCOL; + } + }; + + let expected = Signature::::serialized_len(); + if out_sig_len < expected { + set_last_error(format!( + "output buffer too small: need {expected}, got {out_sig_len}" + )); + return CGGMP21_ERR_INVALID_ARG; + } + let mut buf = vec![0u8; expected]; + sig.write_to_slice(&mut buf); + std::ptr::copy_nonoverlapping(buf.as_ptr(), out_sig, buf.len()); + CGGMP21_OK +} diff --git a/tss/cggmp21/cggmp21.go b/tss/cggmp21/cggmp21.go new file mode 100644 index 00000000..7b8c98dd --- /dev/null +++ b/tss/cggmp21/cggmp21.go @@ -0,0 +1,110 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +// Package cggmp21 is a Go wrapper around the cggmp21-ffi static library +// (rust/cggmp21-ffi). It exposes the threshold-ECDSA signing flow as two +// session types — SigningSession (one-shot online signing) and PresignSession +// (offline presignature generation) — plus the local PartialSign and Combine +// helpers that implement the one-round signing variant. +// +// The Rust static library at rust/lib/libcggmp21_ffi.a must be built first: +// +// make build-ffi +package cggmp21 + +/* +#cgo CFLAGS: -I${SRCDIR}/../../rust/include +#cgo LDFLAGS: -L${SRCDIR}/../../rust/lib -lcggmp21_ffi -lm -ldl -lpthread +#include +#include "cggmp21.h" +*/ +import "C" + +import ( + "errors" + "fmt" + "runtime" + "unsafe" +) + +// Message is a single protocol message produced or consumed by a session. +// Recipient == -1 means broadcast; otherwise it is the destination signer's +// 0-based index. +type Message struct { + Recipient int32 + Payload []byte +} + +// IsBroadcast reports whether the message is destined for all parties. +func (m Message) IsBroadcast() bool { return m.Recipient == -1 } + +// SignatureLen is the byte length of a serialised secp256k1 signature (r || s). +var SignatureLen = int(C.cggmp21_signature_len()) + +// Error is returned by FFI calls that fail with a CGGMP21_ERR_* status. The +// Code field exposes the raw status so callers can branch on specific failure +// modes (e.g. "not finished"). +type Error struct { + Code int + Message string +} + +func (e *Error) Error() string { + return fmt.Sprintf("cggmp21 (rc=%d): %s", e.Code, e.Message) +} + +// cgoCall invokes a C function that returns a CGGMP21_* status code. On a +// non-OK return it wraps cggmp21_last_error() into an *Error. LockOSThread +// keeps the goroutine pinned across the call+error read so the thread-local +// error storage stays consistent. +func cgoCall(call func() C.int) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + rc := call() + if rc == C.CGGMP21_OK { + return nil + } + return &Error{ + Code: int(rc), + Message: C.GoString(C.cggmp21_last_error()), + } +} + +// isNotFinished reports whether err signals that the protocol has not yet +// produced a result. +func isNotFinished(err error) bool { + var e *Error + return errors.As(err, &e) && e.Code == int(C.CGGMP21_ERR_NOT_FINISHED) +} + +// bytePtr returns a *C.uint8_t for the given Go slice. For empty slices it +// returns nil so we never deref a zero-length slice. +func bytePtr(b []byte) *C.uint8_t { + if len(b) == 0 { + return nil + } + return (*C.uint8_t)(unsafe.Pointer(&b[0])) +} + +// uint16Ptr returns a *C.uint16_t for the given slice, or nil if empty. +func uint16Ptr(s []uint16) *C.uint16_t { + if len(s) == 0 { + return nil + } + return (*C.uint16_t)(unsafe.Pointer(&s[0])) +} + +// copyAndFree turns an FFI-allocated (out_json, out_len) pair into a Go []byte +// and releases the C buffer. +func copyAndFree(ptr *C.uint8_t, length C.size_t) []byte { + if ptr == nil || length == 0 { + return nil + } + out := C.GoBytes(unsafe.Pointer(ptr), C.int(length)) + C.cggmp21_free_buffer(ptr, length) + return out +} + +// ErrNotFinished is returned when a result accessor is called before the +// protocol has completed. +var ErrNotFinished = errors.New("cggmp21: protocol not finished") diff --git a/tss/cggmp21/cggmp21_test.go b/tss/cggmp21/cggmp21_test.go new file mode 100644 index 00000000..3556e6e4 --- /dev/null +++ b/tss/cggmp21/cggmp21_test.go @@ -0,0 +1,203 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package cggmp21 + +import ( + "errors" + "strings" + "testing" +) + +// ── Constants ──────────────────────────────────────────────────────────────── + +func TestSignatureLen(t *testing.T) { + if SignatureLen != 64 { + t.Fatalf("SignatureLen = %d, want 64 (secp256k1 r||s)", SignatureLen) + } +} + +// ── Message ───────────────────────────────────────────────────────────────── + +func TestMessage_IsBroadcast(t *testing.T) { + for _, tc := range []struct { + name string + recipient int32 + want bool + }{ + {"broadcast", -1, true}, + {"party 0", 0, false}, + {"party 5", 5, false}, + } { + t.Run(tc.name, func(t *testing.T) { + m := Message{Recipient: tc.recipient} + if got := m.IsBroadcast(); got != tc.want { + t.Fatalf("Recipient=%d → IsBroadcast()=%v, want %v", tc.recipient, got, tc.want) + } + }) + } +} + +// ── Error type ────────────────────────────────────────────────────────────── + +func TestError_FormatAndUnwrap(t *testing.T) { + e := &Error{Code: 2, Message: "boom"} + if msg := e.Error(); !strings.Contains(msg, "rc=2") || !strings.Contains(msg, "boom") { + t.Fatalf("Error string %q missing expected fields", msg) + } + + var target *Error + if !errors.As(e, &target) { + t.Fatal("errors.As did not unwrap *Error") + } + if target.Code != 2 { + t.Fatalf("unwrapped Code = %d, want 2", target.Code) + } +} + +// ── Input validation ──────────────────────────────────────────────────────── + +func TestNewSigningSession_RejectsBadInputs(t *testing.T) { + for _, tc := range []struct { + name string + keyShare []byte + eid []byte + i uint16 + parties []uint16 + dataHash []byte + wantSubstring string + wantErrorClass string // "go" for Go-side check, "ffi" for FFI error code + }{ + { + name: "empty parties", + keyShare: []byte("{}"), + eid: []byte("eid"), + parties: nil, + dataHash: make([]byte, 32), + wantSubstring: "partiesIndexes", + wantErrorClass: "go", + }, + { + name: "wrong-size hash", + keyShare: []byte("{}"), + eid: []byte("eid"), + parties: []uint16{0}, + dataHash: make([]byte, 31), + wantSubstring: "32 bytes", + wantErrorClass: "go", + }, + { + name: "malformed keyshare JSON", + keyShare: []byte("not json"), + eid: []byte("eid"), + parties: []uint16{0, 1}, + dataHash: make([]byte, 32), + wantSubstring: "deserialize", + wantErrorClass: "ffi", + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := NewSigningSession(tc.keyShare, tc.eid, tc.i, tc.parties, tc.dataHash) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tc.wantSubstring) { + t.Fatalf("error %q does not contain %q", err.Error(), tc.wantSubstring) + } + var ffiErr *Error + isFFI := errors.As(err, &ffiErr) + if tc.wantErrorClass == "ffi" && !isFFI { + t.Fatalf("expected *Error, got %T", err) + } + if tc.wantErrorClass == "go" && isFFI { + t.Fatalf("expected Go-side error, got *Error: %v", ffiErr) + } + }) + } +} + +func TestNewPresignSession_RejectsBadInputs(t *testing.T) { + if _, err := NewPresignSession([]byte("{}"), []byte("eid"), 0, nil); err == nil { + t.Fatal("expected error for empty partiesIndexes") + } + if _, err := NewPresignSession([]byte("not json"), []byte("eid"), 0, []uint16{0, 1}); err == nil { + t.Fatal("expected error for malformed keyshare") + } +} + +func TestPartialSign_RejectsBadInputs(t *testing.T) { + for _, tc := range []struct { + name string + presig []byte + hash []byte + wantContains string + }{ + {"empty presig", nil, make([]byte, 32), "presignature"}, + {"short hash", []byte("{}"), make([]byte, 16), "32 bytes"}, + {"malformed presig", []byte("garbage"), make([]byte, 32), "deserialize"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, err := PartialSign(tc.presig, tc.hash) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), tc.wantContains) { + t.Fatalf("error %q does not contain %q", err.Error(), tc.wantContains) + } + }) + } +} + +func TestCombine_RejectsBadInputs(t *testing.T) { + if _, err := Combine(nil); err == nil { + t.Fatal("expected error for nil partials") + } + if _, err := Combine([][]byte{}); err == nil { + t.Fatal("expected error for empty partials slice") + } + if _, err := Combine([][]byte{[]byte("not json")}); err == nil { + t.Fatal("expected error for malformed partial") + } +} + +// ── Session lifecycle (no live session — methods on a nil-ptr session) ───── + +func TestSigningSession_MethodsAfterClose(t *testing.T) { + // Build a session shell with a nil ptr — equivalent to a closed session. + s := &SigningSession{} + + if err := s.Deliver(0, false, []byte("x")); err == nil { + t.Error("Deliver should error on closed session") + } + if _, _, err := s.NextOutgoing(); err == nil { + t.Error("NextOutgoing should error on closed session") + } + if s.Done() { + t.Error("Done should be false on closed session") + } + if _, err := s.Signature(); err == nil { + t.Error("Signature should error on closed session") + } + // Close on already-closed must be a no-op. + s.Close() + s.Close() +} + +func TestPresignSession_MethodsAfterClose(t *testing.T) { + s := &PresignSession{} + + if err := s.Deliver(0, false, []byte("x")); err == nil { + t.Error("Deliver should error on closed session") + } + if _, _, err := s.NextOutgoing(); err == nil { + t.Error("NextOutgoing should error on closed session") + } + if s.Done() { + t.Error("Done should be false on closed session") + } + if _, err := s.Presignature(); err == nil { + t.Error("Presignature should error on closed session") + } + s.Close() + s.Close() +} diff --git a/tss/cggmp21/e2e_test.go b/tss/cggmp21/e2e_test.go new file mode 100644 index 00000000..a0f98165 --- /dev/null +++ b/tss/cggmp21/e2e_test.go @@ -0,0 +1,275 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package cggmp21 + +import ( + "crypto/sha256" + "os" + "path/filepath" + "testing" + + "github.com/ethereum/go-ethereum/crypto" +) + +// loadFixtures returns the three pre-generated 2-of-3 key shares and the +// 33-byte compressed shared public key. Regenerate with: +// +// cargo run --release --manifest-path rust/Cargo.toml --example gen_fixtures \ +// -- tss/cggmp21/testdata +func loadFixtures(t *testing.T) (shares [3][]byte, pubkey []byte) { + t.Helper() + for i := 0; i < 3; i++ { + path := filepath.Join("testdata", "share-"+string(rune('0'+i))+".json") + b, err := os.ReadFile(path) + if err != nil { + t.Fatalf("load fixture %s: %v (run gen_fixtures example to regenerate)", path, err) + } + shares[i] = b + } + pk, err := os.ReadFile(filepath.Join("testdata", "pubkey.bin")) + if err != nil { + t.Fatalf("load pubkey fixture: %v", err) + } + if len(pk) != 33 { + t.Fatalf("pubkey fixture: want 33 bytes (compressed secp256k1), got %d", len(pk)) + } + return shares, pk +} + +// session is the common interface satisfied by both *SigningSession and +// *PresignSession — lets us share the message-routing loop. +type session interface { + Deliver(sender uint16, broadcast bool, msg []byte) error + NextOutgoing() (Message, bool, error) + Done() bool +} + +// runProtocol drives a set of sessions in lock-step, routing every outgoing +// message to its destination(s) until every session reports Done. Returns the +// total number of rounds it took, for the test log. +func runProtocol(t *testing.T, sessions []session) int { + t.Helper() + type pending struct { + sender uint16 + broadcast bool + payload []byte + } + inboxes := make([][]pending, len(sessions)) + + const maxRounds = 50 // safety cap so a stuck test fails instead of hanging + for round := 0; round < maxRounds; round++ { + // Drain outgoing from each party. + for i, sess := range sessions { + for { + msg, ok, err := sess.NextOutgoing() + if err != nil { + t.Fatalf("party %d NextOutgoing: %v", i, err) + } + if !ok { + break + } + if msg.IsBroadcast() { + for j := range sessions { + if j == i { + continue + } + inboxes[j] = append(inboxes[j], pending{ + sender: uint16(i), + broadcast: true, + payload: msg.Payload, + }) + } + } else { + inboxes[msg.Recipient] = append(inboxes[msg.Recipient], pending{ + sender: uint16(i), + broadcast: false, + payload: msg.Payload, + }) + } + } + } + + // All done? + allDone := true + for _, s := range sessions { + if !s.Done() { + allDone = false + break + } + } + if allDone { + return round + } + + // Detect deadlock — nothing to deliver, nobody done. + anyPending := false + for _, ib := range inboxes { + if len(ib) > 0 { + anyPending = true + break + } + } + if !anyPending { + t.Fatalf("protocol stuck at round %d: no outgoing messages and no parties done", round) + } + + // Deliver everything queued. + for i, ib := range inboxes { + for _, p := range ib { + if err := sessions[i].Deliver(p.sender, p.broadcast, p.payload); err != nil { + t.Fatalf("party %d Deliver: %v", i, err) + } + } + inboxes[i] = nil + } + } + t.Fatalf("protocol did not finish within %d rounds", maxRounds) + return -1 +} + +// TestSigningSession_E2E runs the full 2-of-3 cggmp21 online signing protocol +// between two parties, in-process, and verifies the resulting ECDSA signature +// against the shared public key. +func TestSigningSession_E2E(t *testing.T) { + shares, pubkey := loadFixtures(t) + + // Parties at keygen-index 0 and 1 cooperate; party with share-2 sits out. + signers := []uint16{0, 1} + eid := []byte("cggmp21-go-test-signing-eid-v1") + hash := sha256.Sum256([]byte("hello, threshold signing")) + + s0, err := NewSigningSession(shares[0], eid, 0, signers, hash[:]) + if err != nil { + t.Fatalf("party 0 NewSigningSession: %v", err) + } + defer s0.Close() + s1, err := NewSigningSession(shares[1], eid, 1, signers, hash[:]) + if err != nil { + t.Fatalf("party 1 NewSigningSession: %v", err) + } + defer s1.Close() + + rounds := runProtocol(t, []session{s0, s1}) + t.Logf("signing finished in %d rounds", rounds) + + sig, err := s0.Signature() + if err != nil { + t.Fatalf("Signature: %v", err) + } + if len(sig) != SignatureLen { + t.Fatalf("signature length = %d, want %d", len(sig), SignatureLen) + } + + // Both parties should yield the same signature. + sig1, err := s1.Signature() + if err != nil { + t.Fatalf("party 1 Signature: %v", err) + } + if string(sig) != string(sig1) { + t.Fatal("party 0 and party 1 produced different signatures") + } + + if !crypto.VerifySignature(pubkey, hash[:], sig) { + t.Fatalf("signature does not verify against shared pubkey\n sig=%x\n hash=%x\n pubkey=%x", sig, hash[:], pubkey) + } +} + +// TestPresignPartialCombine_E2E exercises the one-round signing flow: run the +// presign MPC, locally compute partial signatures with PartialSign, combine +// them with Combine, then verify the final signature. +func TestPresignPartialCombine_E2E(t *testing.T) { + shares, pubkey := loadFixtures(t) + + signers := []uint16{0, 1} + eid := []byte("cggmp21-go-test-presign-eid-v1") + + p0, err := NewPresignSession(shares[0], eid, 0, signers) + if err != nil { + t.Fatalf("party 0 NewPresignSession: %v", err) + } + defer p0.Close() + p1, err := NewPresignSession(shares[1], eid, 1, signers) + if err != nil { + t.Fatalf("party 1 NewPresignSession: %v", err) + } + defer p1.Close() + + rounds := runProtocol(t, []session{p0, p1}) + t.Logf("presign finished in %d rounds", rounds) + + presig0, err := p0.Presignature() + if err != nil { + t.Fatalf("party 0 Presignature: %v", err) + } + presig1, err := p1.Presignature() + if err != nil { + t.Fatalf("party 1 Presignature: %v", err) + } + + // Sign a message that wasn't known at presign time. + hash := sha256.Sum256([]byte("offline-signed message")) + + partial0, err := PartialSign(presig0, hash[:]) + if err != nil { + t.Fatalf("PartialSign party 0: %v", err) + } + partial1, err := PartialSign(presig1, hash[:]) + if err != nil { + t.Fatalf("PartialSign party 1: %v", err) + } + + sig, err := Combine([][]byte{partial0, partial1}) + if err != nil { + t.Fatalf("Combine: %v", err) + } + if len(sig) != SignatureLen { + t.Fatalf("signature length = %d, want %d", len(sig), SignatureLen) + } + + if !crypto.VerifySignature(pubkey, hash[:], sig) { + t.Fatalf("combined signature does not verify against shared pubkey\n sig=%x\n hash=%x\n pubkey=%x", sig, hash[:], pubkey) + } +} + +// TestPartialSign_RejectsReusedPresignature is informational: it documents the +// "never reuse a presignature" invariant. The library currently does not +// enforce this — reusing a presignature succeeds at the FFI level — so this +// test simply checks that PartialSign on the same presignature twice produces +// outputs (proving the leak path exists). The real protection is at the +// caller layer: discard a presignature after one use. +func TestPartialSign_DoesNotReusePresignature(t *testing.T) { + shares, _ := loadFixtures(t) + + signers := []uint16{0, 1} + eid := []byte("cggmp21-go-test-reuse-eid-v1") + + p0, err := NewPresignSession(shares[0], eid, 0, signers) + if err != nil { + t.Fatalf("party 0 NewPresignSession: %v", err) + } + defer p0.Close() + p1, err := NewPresignSession(shares[1], eid, 1, signers) + if err != nil { + t.Fatalf("party 1 NewPresignSession: %v", err) + } + defer p1.Close() + + runProtocol(t, []session{p0, p1}) + + presig, err := p0.Presignature() + if err != nil { + t.Fatalf("Presignature: %v", err) + } + + h1 := sha256.Sum256([]byte("first message")) + h2 := sha256.Sum256([]byte("second message")) + if _, err := PartialSign(presig, h1[:]); err != nil { + t.Fatal(err) + } + if _, err := PartialSign(presig, h2[:]); err != nil { + // Library does not enforce single-use — second call still returns a + // partial. This is documented in the API; the caller must discard. + t.Fatal(err) + } +} diff --git a/tss/cggmp21/presign.go b/tss/cggmp21/presign.go new file mode 100644 index 00000000..fea3fd52 --- /dev/null +++ b/tss/cggmp21/presign.go @@ -0,0 +1,207 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package cggmp21 + +/* +#include "cggmp21.h" +*/ +import "C" + +import ( + "bytes" + "errors" + "runtime" + "unsafe" +) + +// PresignSession drives the cggmp21 offline presignature-generation protocol +// (the message-independent half of the one-round signing flow). Drive it the +// same way as SigningSession and fetch the JSON-encoded Presignature with +// Presignature once Done reports true. +type PresignSession struct { + ptr *C.CggmpPresignSession +} + +// NewPresignSession starts a presignature-generation session. Inputs match +// NewSigningSession minus the message hash — presignatures are +// message-independent. +func NewPresignSession(keyShare, eid []byte, i uint16, partiesIndexes []uint16) (*PresignSession, error) { + if len(partiesIndexes) == 0 { + return nil, errors.New("cggmp21: partiesIndexes must not be empty") + } + + var ptr *C.CggmpPresignSession + err := cgoCall(func() C.int { + return C.cggmp21_presign_new( + bytePtr(keyShare), C.size_t(len(keyShare)), + bytePtr(eid), C.size_t(len(eid)), + C.uint16_t(i), + uint16Ptr(partiesIndexes), C.size_t(len(partiesIndexes)), + &ptr, + ) + }) + if err != nil { + return nil, err + } + + s := &PresignSession{ptr: ptr} + runtime.SetFinalizer(s, (*PresignSession).finalize) + return s, nil +} + +// Deliver hands an incoming protocol message to the state machine. +func (s *PresignSession) Deliver(sender uint16, broadcast bool, msg []byte) error { + if s.ptr == nil { + return errors.New("cggmp21: presign session closed") + } + var b C.uint8_t + if broadcast { + b = 1 + } + return cgoCall(func() C.int { + return C.cggmp21_presign_deliver( + s.ptr, + C.uint16_t(sender), + b, + bytePtr(msg), C.size_t(len(msg)), + ) + }) +} + +// NextOutgoing pops the next queued outgoing message. +func (s *PresignSession) NextOutgoing() (Message, bool, error) { + if s.ptr == nil { + return Message{}, false, errors.New("cggmp21: presign session closed") + } + var ( + recipient C.int32_t + buf *C.uint8_t + length C.size_t + ) + runtime.LockOSThread() + defer runtime.UnlockOSThread() + rc := C.cggmp21_presign_poll_outgoing(s.ptr, &recipient, &buf, &length) + if rc == 0 { + return Message{}, false, nil + } + return Message{ + Recipient: int32(recipient), + Payload: copyAndFree(buf, length), + }, true, nil +} + +// Done reports whether the presignature protocol has finished. +func (s *PresignSession) Done() bool { + if s.ptr == nil { + return false + } + return C.cggmp21_presign_is_done(s.ptr) != 0 +} + +// Presignature returns the JSON-serialised Presignature once Done is true. +// Pass this to PartialSign together with the message hash to compute a +// PartialSignature. **Never reuse a presignature** — doing so leaks the +// private key. +func (s *PresignSession) Presignature() ([]byte, error) { + if s.ptr == nil { + return nil, errors.New("cggmp21: presign session closed") + } + var ( + buf *C.uint8_t + length C.size_t + ) + err := cgoCall(func() C.int { + return C.cggmp21_presign_result(s.ptr, &buf, &length) + }) + if err != nil { + if isNotFinished(err) { + return nil, ErrNotFinished + } + return nil, err + } + return copyAndFree(buf, length), nil +} + +// Close releases the C-side session. Safe to call multiple times. +func (s *PresignSession) Close() { + runtime.SetFinalizer(s, nil) + s.free() +} + +func (s *PresignSession) finalize() { + s.free() +} + +func (s *PresignSession) free() { + if s.ptr == nil { + return + } + C.cggmp21_presign_free(s.ptr) + s.ptr = nil +} + +// PartialSign locally computes a PartialSignature from a presignature and a +// 32-byte message hash. The returned bytes are JSON-serialised and meant to be +// gathered (one per signer) and passed to Combine. +// +// **Never reuse a presignature.** +func PartialSign(presignature, dataHash []byte) ([]byte, error) { + if len(dataHash) != 32 { + return nil, errors.New("cggmp21: dataHash must be 32 bytes") + } + if len(presignature) == 0 { + return nil, errors.New("cggmp21: presignature must not be empty") + } + + var ( + buf *C.uint8_t + length C.size_t + ) + err := cgoCall(func() C.int { + return C.cggmp21_partial_sign( + bytePtr(presignature), C.size_t(len(presignature)), + bytePtr(dataHash), C.size_t(len(dataHash)), + &buf, &length, + ) + }) + if err != nil { + return nil, err + } + return copyAndFree(buf, length), nil +} + +// Combine combines a threshold-sized set of PartialSignatures (each JSON- +// serialised, as returned by PartialSign) into a final ECDSA signature +// (r || s, SignatureLen bytes). +// +// The returned signature may still be invalid for the public key and message +// if some signer cheated; callers should verify it before trusting. +func Combine(partials [][]byte) ([]byte, error) { + if len(partials) == 0 { + return nil, errors.New("cggmp21: at least one partial signature required") + } + + var buf bytes.Buffer + buf.WriteByte('[') + for i, p := range partials { + if i > 0 { + buf.WriteByte(',') + } + buf.Write(p) + } + buf.WriteByte(']') + arr := buf.Bytes() + + out := make([]byte, SignatureLen) + err := cgoCall(func() C.int { + return C.cggmp21_combine_partials( + bytePtr(arr), C.size_t(len(arr)), + (*C.uint8_t)(unsafe.Pointer(&out[0])), C.size_t(len(out)), + ) + }) + if err != nil { + return nil, err + } + return out, nil +} diff --git a/tss/cggmp21/signing.go b/tss/cggmp21/signing.go new file mode 100644 index 00000000..be645f9b --- /dev/null +++ b/tss/cggmp21/signing.go @@ -0,0 +1,168 @@ +// The Licensed Work is (c) 2022 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +package cggmp21 + +/* +#include "cggmp21.h" +*/ +import "C" + +import ( + "errors" + "runtime" + "sync" + "unsafe" +) + +// SigningSession drives the cggmp21 online ECDSA signing protocol for one +// party. Construct it with NewSigningSession, drive it by alternating Deliver +// and NextOutgoing until Done reports true, then fetch the result with +// Signature. +// +// A session owns C-side resources; call Close (or rely on the finalizer, which +// is best-effort) to release them. The session is not safe for concurrent use +// from multiple goroutines. +type SigningSession struct { + mu sync.Mutex + ptr *C.CggmpSigningSession +} + +// NewSigningSession starts a signing session for this party. +// +// Parameters: +// - keyShare: JSON-serialised KeyShare. +// - eid: execution ID bytes (must be unique per protocol run). +// - i: this party's signing index (0-based, must be < len(partiesIndexes)). +// - partiesIndexes: partiesIndexes[j] is the keygen index of the j-th signer. +// - dataHash: 32-byte big-endian message hash (e.g. Keccak-256 output). +func NewSigningSession(keyShare, eid []byte, i uint16, partiesIndexes []uint16, dataHash []byte) (*SigningSession, error) { + if len(dataHash) != 32 { + return nil, errors.New("cggmp21: dataHash must be 32 bytes") + } + if len(partiesIndexes) == 0 { + return nil, errors.New("cggmp21: partiesIndexes must not be empty") + } + + var ptr *C.CggmpSigningSession + err := cgoCall(func() C.int { + return C.cggmp21_signing_new( + bytePtr(keyShare), C.size_t(len(keyShare)), + bytePtr(eid), C.size_t(len(eid)), + C.uint16_t(i), + uint16Ptr(partiesIndexes), C.size_t(len(partiesIndexes)), + bytePtr(dataHash), C.size_t(len(dataHash)), + &ptr, + ) + }) + if err != nil { + return nil, err + } + + s := &SigningSession{ptr: ptr} + runtime.SetFinalizer(s, (*SigningSession).finalize) + return s, nil +} + +// Deliver hands an incoming protocol message to the state machine. Drain any +// queued outgoing messages with NextOutgoing after each Deliver. +func (s *SigningSession) Deliver(sender uint16, broadcast bool, msg []byte) error { + s.mu.Lock() + defer s.mu.Unlock() + if s.ptr == nil { + return errors.New("cggmp21: signing session closed") + } + var b C.uint8_t + if broadcast { + b = 1 + } + return cgoCall(func() C.int { + return C.cggmp21_signing_deliver( + s.ptr, + C.uint16_t(sender), + b, + bytePtr(msg), C.size_t(len(msg)), + ) + }) +} + +// NextOutgoing pops the next queued outgoing message. The boolean is false +// when the queue is drained. +func (s *SigningSession) NextOutgoing() (Message, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.ptr == nil { + return Message{}, false, errors.New("cggmp21: signing session closed") + } + var ( + recipient C.int32_t + buf *C.uint8_t + length C.size_t + ) + runtime.LockOSThread() + defer runtime.UnlockOSThread() + rc := C.cggmp21_signing_poll_outgoing(s.ptr, &recipient, &buf, &length) + if rc == 0 { + return Message{}, false, nil + } + return Message{ + Recipient: int32(recipient), + Payload: copyAndFree(buf, length), + }, true, nil +} + +// Done reports whether the protocol has finished (successfully or with an +// error). Once Done returns true, call Signature to fetch the result. +func (s *SigningSession) Done() bool { + s.mu.Lock() + defer s.mu.Unlock() + if s.ptr == nil { + return false + } + return C.cggmp21_signing_is_done(s.ptr) != 0 +} + +// Signature returns the final ECDSA signature as r || s (big-endian, +// SignatureLen bytes). Returns ErrNotFinished if Done has not yet reported true. +func (s *SigningSession) Signature() ([]byte, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.ptr == nil { + return nil, errors.New("cggmp21: signing session closed") + } + out := make([]byte, SignatureLen) + err := cgoCall(func() C.int { + return C.cggmp21_signing_result(s.ptr, (*C.uint8_t)(unsafe.Pointer(&out[0])), C.size_t(len(out))) + }) + if err != nil { + // Distinguish "not finished" from other errors for callers that want + // to retry until the protocol completes. + if isNotFinished(err) { + return nil, ErrNotFinished + } + return nil, err + } + return out, nil +} + +// Close releases the C-side session. Safe to call multiple times. +func (s *SigningSession) Close() { + s.mu.Lock() + defer s.mu.Unlock() + runtime.SetFinalizer(s, nil) + s.freeLocked() +} + +func (s *SigningSession) finalize() { + s.mu.Lock() + defer s.mu.Unlock() + s.freeLocked() +} + +func (s *SigningSession) freeLocked() { + if s.ptr == nil { + return + } + C.cggmp21_signing_free(s.ptr) + s.ptr = nil +} diff --git a/tss/cggmp21/testdata/pubkey.bin b/tss/cggmp21/testdata/pubkey.bin new file mode 100644 index 00000000..02151304 --- /dev/null +++ b/tss/cggmp21/testdata/pubkey.bin @@ -0,0 +1 @@ + c$~Yc hKhNI \ No newline at end of file diff --git a/tss/cggmp21/testdata/share-0.json b/tss/cggmp21/testdata/share-0.json new file mode 100644 index 00000000..c4130136 --- /dev/null +++ b/tss/cggmp21/testdata/share-0.json @@ -0,0 +1,81 @@ +{ + "core": { + "curve": "secp256k1", + "i": 0, + "shared_public_key": "03910c0c6391247e94f9e359bea4b5f2630c90cc68124b056814a5d34e969449e2", + "public_shares": [ + "039bbf107d3d6c52543b23b17a235b18b9e27a00401c29d6dcfd8c36b2eae2af80", + "0354513c526919217c83d3d6364ed7bcf9a4d935caeae1bb4c954e661e3cc833c3", + "02e99ddc7b27efb95afb920f17cb80ac36e1817979caeb7256bc068eabb84a5a24" + ], + "vss_setup": { + "min_signers": 2, + "I": [ + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000003" + ] + }, + "x": "134de2b86c49f015db2ea6b87f2e85ebafea36c8122b5433ded467282b686c4b" + }, + "aux": { + "p": { + "radix": 16, + "value": "e7dae1d2f201f402d6ffa55041c374ed41d0e85700db85d8c25984bde184a27be631a5c1fe6efa5b9ade36ce68f4e71ee986ea135e50ac5587c13eafadcdcf1f2db0635f9c4a0be1a17287b54df3f6d4c4ac2b768a3de625eb22cd24d5ab9a400c4dd48452cfcadd8867ac30084bb9ead551cfeab07394b9cbff6eb4d3e638c3" + }, + "q": { + "radix": 16, + "value": "cdc424599b8e5f8bea7768909465888ff573a8dd30f577059150c3754b11add0deeab029a596c94fd44c6f71397cc8dd40c6009dd0a0ebfa926b8a21b4ad11a9ffd184020559f48b8f11d73f486d17cb73ec44331e6a3d1ef39b92d6b8b7abe0bfa56b8591454d9a3a487f37d267e04be49c9d026b17a615f07fbbd624f4f75b" + }, + "parties": [ + { + "N": { + "radix": 16, + "value": "ba5beb56c1097ef06edf175d621fb72a69fb216ffdf38e322d544735ae4e8da17fc996bafa8ea820f963a0c3dd1d45cee5de123ec7e5a5b33e8e4f07a99ec447ca336c612dec67fd32b501d21032ce65b3a0e0073b7c9f4599caa92b5bb159131befa3dd2aab5d1f8778186f1c602a7c910f923d5f0ba5cf1009bc75c74085a432a290704065cf81cddadd3b014c8e660fde87bbe605fb23ec132566dd70fa431ba51b6cd39ba49fade14fe130930aedc9fdd6464cd0a79d2b99100617c695725dcc62fd03c0a02982e93f003b525854863b389452fc02bcf92b51523e0fd8d17fac9072093349a4bda9998a027e9370d5dc20c0f4532d44306a0e25f9765251" + }, + "s": { + "radix": 16, + "value": "57f1caf00ab4879c89ea92f1f206f56caee51a92772c20c939878480569ab20d273af03054f8fcc22aac46ebab1f3f9e8ebf39eb2616a5dc5a161b6e43c968da6fd4491df3c488013353e429e365a7e4a772bcfdf2e29a7701ef07a84a443e2532658ba27a5966ec44ec6d7103586e9af4ce6acca9f93b7d6a3dea9f79f6db4c92a442f5a862adcbcc245d4da353b0c0967353bcd6e10c19b67933ffb0ca5996115792215708f6c5205df76379b73d220102d8a200a6b16325501950c0422026ef2de268c2d3a169f274263aa2007423187f0b4ecbc5da8ad52653f56959c1c797f2d003f69f45578ee1164bb64b832fae569368c139ad908903d3a37d51704f" + }, + "t": { + "radix": 16, + "value": "7b49f5f7f8266a0e201570b0d41c83bc236bd9464b92762a7db8388d481da1872ee899da52d862aeb8ddd63fcd037496f16510caa239a2e04ee3703d19bdeb4a0eeb057697f6eca59e0b4a66cb1ea596a484644898d98552424d7e748d5f42ee8a07087a434cfd73db3c3f1a2678bcbe256377c60489529949a1251b25c47e19c318024bde5bb8755edbbad88609bb40806ad77d8e72ccabd49443f147381954d490898e99f8c8e984b242de5c0af5e7b439a9ccc015f3d704a17f146bee033bb71b12cbe57f22a8324f02af5eea0652388696c93f1096ee51bad4f608f8799d18d96ce51a94d529b2fac8efcc673f995b20823a26c943e9c05f1825a5cab96" + }, + "multiexp": null, + "crt": null + }, + { + "N": { + "radix": 16, + "value": "a7ec71b4ab2eea6b27abdd877a6ef15fe12ec9c2bddbdf50a500dc91ca1a35eb6346d96ac5d928280d5da83c6697a91512914aad7161732982855559e98eded81831d35ccc9ba01d445c9789c99c107cddbd516b7657d4a6769809a8cb5a76ebac559f4d4e0341ad8fb06538f8d7dc9378fed58e1ee796207c738f7ce6e3e558aa737648974b3a4a6b290b89d79e4928104d9d87b44d1e7da70ce0146496a15499959bd8d5e05f5f432846497047f060c976a6bf71f75230f1774c44741c82ff8eadd88278b8a84f9087817529fb811067fd5e34d8e4e4df428cd08c3dc85b5813869f00efa90d774a7606a66cee3fae15003552575cf3092c537e2fa28420c9" + }, + "s": { + "radix": 16, + "value": "427a2160b77d4596b4e4fcc7db745e21557fd69d051bc7a7e0830e06b363c975765c7db90992ab9943eba63860a2f8231471e2ad20d559b7ba274cbe5a1c75606f8bb24b1f12f167dc8044ddf261beaeb8a944001a70862e44d8c6e0ce8b870207c74fa5ab51d3869b0778fbaa1f49b20dbdb509b9fc15fdf7c72b63838005e7825973dbace607641774210293dfe10193858d8af831ee80115360756d2f0f0c8918f79ba74b35c1913e549f7e720afe9c2181c850e4a09b186d3018cd5b01ed8238c050496f8ada1d2907a00c7a0de91e9d3e5b7b01ba2d6654c7d0b172009b6e7b1b35cfa6ddcd169fdd495e91f6d92958c457bedca931b5cf0acc5be47853" + }, + "t": { + "radix": 16, + "value": "762f51c1d412541dd6a4553a85f44164c6b0221dc96c45c54114f4fded6246d6879fd3614f7980b89e3f12c46936e126a05174cfd46d10775894f7eb55d613fbd4490f72ab90f63608045f5a31df57acf4543adc693cc7c29d0dc192498b9280050bb8d2f4bd42f322d1c6ea2774b45fecaabf798ce71f28972cff4c761bd87d3987fe4eb15da36229892f209a7ae0f0032daf249d95897b4c2ca89726863e6b0370438f9bab99e0f7f5dd1a0fc2934e46879508c73ab2a82e73a3b23c2b4825ca7deb81e1a816f56beb1708090592aa6276055b167abda36e35db71ba5e601f127d849fe5d08bae8f6b1525e6084f780a1fc0c3315d7ae0c4f859b559a8f888" + }, + "multiexp": null, + "crt": null + }, + { + "N": { + "radix": 16, + "value": "971ddb7f6df3812cd3bcfd9c4c48067fccc88ec112b7789b62c4b1b0ef6b532b94afa0c70cae251280a56d4e4b7d366e995d126606e1a123540d6548cac9477bd515f8ae2a0be374be72f0d2f221a4ed557bdeff1d80b434782458eb7e6aa13da19de98a11f1b42d270cd44bdb30d91902ddd8f6773f14199522cc43bd82b76ebe5a281432d617728465fe55423bb8631f72799f7ce89ed124b050062d322171e1cc45e8ee0ff14b437ccdb064423059c9703871e7c050dd7f48fb9b253b47fdb387e6c00ed3c9444362e169a248868ed103343165a1f85bd652e5c20f84e5a33de696c27e0dd1b306d7ba56cfd936292d36399765b936b1a077e5c687886d19" + }, + "s": { + "radix": 16, + "value": "63a5bef6c9a145d96c7299859757a6e526c853eb60b7e129c807671f85f4041622ce5c3cfc248a75cff2d61384d7223d86a06a14e197fd1aede2e4abd23ed6c670a974353667fee740b4b4f6492defd6992b167a0008b325e87a94f4299696c21f0d0c3666592610c4a9d4c1250ecc6e781768bca404c488c0ff478dd26270a07b32f7843f1201c7b045d3ddfa8262ef3fceaedd0b8ddce3c867c08724f32714e99ccb2fa4a08b92fe87e857c9498b78f142db00c634aff1e211d6a84612352fc982e415898f0c50c3b70e36f5b3cac030c05e15330e1667a0f71d71da5650669053322fdef0521cb8904ee685e0d4caa6a215747d0af77dc58b4b15fef02c56" + }, + "t": { + "radix": 16, + "value": "1803b7469c8f3f6b7e60d0bac8a02b1cafd5e31bd5a448cd15a3e83951dc4963772caa8076a5ce5bf15a8c0bdbccdaf63d05613e9a3d349771186950daf2fd22c693d264901bdfc29aa768e81319cc440dd1b604672c733019d2fd8a3589b492a6dd2cd749df2179413cc1288afaab1f8bc9727272b79423780cb8bcf5fc1cf30c3822b680ce59a4648fa9ad556fad5f2eed98a8f55036e9d47c216aa3f9d9ec9ba9ceb1c988adc13887a254e5cc12ba7acc1fc7234cbcd797817dcf5d1a12865c8bf059b7d3b6748fe8c0abe1cf51a0ab94df2c1201fad711bba0d7c21c6ebb61f223ffa86e49c7022d2d564f9531e5d80e48d91277101780ff77d1c37ccd5e" + }, + "multiexp": null, + "crt": null + } + ] + } +} \ No newline at end of file diff --git a/tss/cggmp21/testdata/share-1.json b/tss/cggmp21/testdata/share-1.json new file mode 100644 index 00000000..8c593ae6 --- /dev/null +++ b/tss/cggmp21/testdata/share-1.json @@ -0,0 +1,81 @@ +{ + "core": { + "curve": "secp256k1", + "i": 1, + "shared_public_key": "03910c0c6391247e94f9e359bea4b5f2630c90cc68124b056814a5d34e969449e2", + "public_shares": [ + "039bbf107d3d6c52543b23b17a235b18b9e27a00401c29d6dcfd8c36b2eae2af80", + "0354513c526919217c83d3d6364ed7bcf9a4d935caeae1bb4c954e661e3cc833c3", + "02e99ddc7b27efb95afb920f17cb80ac36e1817979caeb7256bc068eabb84a5a24" + ], + "vss_setup": { + "min_signers": 2, + "I": [ + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000003" + ] + }, + "x": "5874c4d159f462f4a0b63e82eabeacd9572affa7afacd22b8d4ae856cb1e0f52" + }, + "aux": { + "p": { + "radix": 16, + "value": "aff7061e77eba99252f4105f00d28a146d3960abdd0644a2bcf397062bb80bf6a32735434e3794ec7e284efdccf364fc3ae0a081748a8f0255b1d519a40351b0a2b1d168cb76a478c99fd9fc39a838da75ea2748b72a22291cc33fb6eaf7ff3eb5434c31f007128830287ab92ca494e969e4f59b3c2c3e082087ab4095b0413b" + }, + "q": { + "radix": 16, + "value": "f44d1b056eee3c358e1ab8d8c164870b250e6cb64bbfc6842146ec5b61239ebee6331d84d6bbd222b5152e034f699e3828fb78b7bdad507b3574ad26a03a70713203c3ede679a58d6379ec422ec60f97e00eb8cc12320d2f7dd7e8d5bcf47511de8e5a6b9d25fc9b353999a0dbaf6c81374a47cab333c9661c7085812fcac5cb" + }, + "parties": [ + { + "N": { + "radix": 16, + "value": "ba5beb56c1097ef06edf175d621fb72a69fb216ffdf38e322d544735ae4e8da17fc996bafa8ea820f963a0c3dd1d45cee5de123ec7e5a5b33e8e4f07a99ec447ca336c612dec67fd32b501d21032ce65b3a0e0073b7c9f4599caa92b5bb159131befa3dd2aab5d1f8778186f1c602a7c910f923d5f0ba5cf1009bc75c74085a432a290704065cf81cddadd3b014c8e660fde87bbe605fb23ec132566dd70fa431ba51b6cd39ba49fade14fe130930aedc9fdd6464cd0a79d2b99100617c695725dcc62fd03c0a02982e93f003b525854863b389452fc02bcf92b51523e0fd8d17fac9072093349a4bda9998a027e9370d5dc20c0f4532d44306a0e25f9765251" + }, + "s": { + "radix": 16, + "value": "57f1caf00ab4879c89ea92f1f206f56caee51a92772c20c939878480569ab20d273af03054f8fcc22aac46ebab1f3f9e8ebf39eb2616a5dc5a161b6e43c968da6fd4491df3c488013353e429e365a7e4a772bcfdf2e29a7701ef07a84a443e2532658ba27a5966ec44ec6d7103586e9af4ce6acca9f93b7d6a3dea9f79f6db4c92a442f5a862adcbcc245d4da353b0c0967353bcd6e10c19b67933ffb0ca5996115792215708f6c5205df76379b73d220102d8a200a6b16325501950c0422026ef2de268c2d3a169f274263aa2007423187f0b4ecbc5da8ad52653f56959c1c797f2d003f69f45578ee1164bb64b832fae569368c139ad908903d3a37d51704f" + }, + "t": { + "radix": 16, + "value": "7b49f5f7f8266a0e201570b0d41c83bc236bd9464b92762a7db8388d481da1872ee899da52d862aeb8ddd63fcd037496f16510caa239a2e04ee3703d19bdeb4a0eeb057697f6eca59e0b4a66cb1ea596a484644898d98552424d7e748d5f42ee8a07087a434cfd73db3c3f1a2678bcbe256377c60489529949a1251b25c47e19c318024bde5bb8755edbbad88609bb40806ad77d8e72ccabd49443f147381954d490898e99f8c8e984b242de5c0af5e7b439a9ccc015f3d704a17f146bee033bb71b12cbe57f22a8324f02af5eea0652388696c93f1096ee51bad4f608f8799d18d96ce51a94d529b2fac8efcc673f995b20823a26c943e9c05f1825a5cab96" + }, + "multiexp": null, + "crt": null + }, + { + "N": { + "radix": 16, + "value": "a7ec71b4ab2eea6b27abdd877a6ef15fe12ec9c2bddbdf50a500dc91ca1a35eb6346d96ac5d928280d5da83c6697a91512914aad7161732982855559e98eded81831d35ccc9ba01d445c9789c99c107cddbd516b7657d4a6769809a8cb5a76ebac559f4d4e0341ad8fb06538f8d7dc9378fed58e1ee796207c738f7ce6e3e558aa737648974b3a4a6b290b89d79e4928104d9d87b44d1e7da70ce0146496a15499959bd8d5e05f5f432846497047f060c976a6bf71f75230f1774c44741c82ff8eadd88278b8a84f9087817529fb811067fd5e34d8e4e4df428cd08c3dc85b5813869f00efa90d774a7606a66cee3fae15003552575cf3092c537e2fa28420c9" + }, + "s": { + "radix": 16, + "value": "427a2160b77d4596b4e4fcc7db745e21557fd69d051bc7a7e0830e06b363c975765c7db90992ab9943eba63860a2f8231471e2ad20d559b7ba274cbe5a1c75606f8bb24b1f12f167dc8044ddf261beaeb8a944001a70862e44d8c6e0ce8b870207c74fa5ab51d3869b0778fbaa1f49b20dbdb509b9fc15fdf7c72b63838005e7825973dbace607641774210293dfe10193858d8af831ee80115360756d2f0f0c8918f79ba74b35c1913e549f7e720afe9c2181c850e4a09b186d3018cd5b01ed8238c050496f8ada1d2907a00c7a0de91e9d3e5b7b01ba2d6654c7d0b172009b6e7b1b35cfa6ddcd169fdd495e91f6d92958c457bedca931b5cf0acc5be47853" + }, + "t": { + "radix": 16, + "value": "762f51c1d412541dd6a4553a85f44164c6b0221dc96c45c54114f4fded6246d6879fd3614f7980b89e3f12c46936e126a05174cfd46d10775894f7eb55d613fbd4490f72ab90f63608045f5a31df57acf4543adc693cc7c29d0dc192498b9280050bb8d2f4bd42f322d1c6ea2774b45fecaabf798ce71f28972cff4c761bd87d3987fe4eb15da36229892f209a7ae0f0032daf249d95897b4c2ca89726863e6b0370438f9bab99e0f7f5dd1a0fc2934e46879508c73ab2a82e73a3b23c2b4825ca7deb81e1a816f56beb1708090592aa6276055b167abda36e35db71ba5e601f127d849fe5d08bae8f6b1525e6084f780a1fc0c3315d7ae0c4f859b559a8f888" + }, + "multiexp": null, + "crt": null + }, + { + "N": { + "radix": 16, + "value": "971ddb7f6df3812cd3bcfd9c4c48067fccc88ec112b7789b62c4b1b0ef6b532b94afa0c70cae251280a56d4e4b7d366e995d126606e1a123540d6548cac9477bd515f8ae2a0be374be72f0d2f221a4ed557bdeff1d80b434782458eb7e6aa13da19de98a11f1b42d270cd44bdb30d91902ddd8f6773f14199522cc43bd82b76ebe5a281432d617728465fe55423bb8631f72799f7ce89ed124b050062d322171e1cc45e8ee0ff14b437ccdb064423059c9703871e7c050dd7f48fb9b253b47fdb387e6c00ed3c9444362e169a248868ed103343165a1f85bd652e5c20f84e5a33de696c27e0dd1b306d7ba56cfd936292d36399765b936b1a077e5c687886d19" + }, + "s": { + "radix": 16, + "value": "63a5bef6c9a145d96c7299859757a6e526c853eb60b7e129c807671f85f4041622ce5c3cfc248a75cff2d61384d7223d86a06a14e197fd1aede2e4abd23ed6c670a974353667fee740b4b4f6492defd6992b167a0008b325e87a94f4299696c21f0d0c3666592610c4a9d4c1250ecc6e781768bca404c488c0ff478dd26270a07b32f7843f1201c7b045d3ddfa8262ef3fceaedd0b8ddce3c867c08724f32714e99ccb2fa4a08b92fe87e857c9498b78f142db00c634aff1e211d6a84612352fc982e415898f0c50c3b70e36f5b3cac030c05e15330e1667a0f71d71da5650669053322fdef0521cb8904ee685e0d4caa6a215747d0af77dc58b4b15fef02c56" + }, + "t": { + "radix": 16, + "value": "1803b7469c8f3f6b7e60d0bac8a02b1cafd5e31bd5a448cd15a3e83951dc4963772caa8076a5ce5bf15a8c0bdbccdaf63d05613e9a3d349771186950daf2fd22c693d264901bdfc29aa768e81319cc440dd1b604672c733019d2fd8a3589b492a6dd2cd749df2179413cc1288afaab1f8bc9727272b79423780cb8bcf5fc1cf30c3822b680ce59a4648fa9ad556fad5f2eed98a8f55036e9d47c216aa3f9d9ec9ba9ceb1c988adc13887a254e5cc12ba7acc1fc7234cbcd797817dcf5d1a12865c8bf059b7d3b6748fe8c0abe1cf51a0ab94df2c1201fad711bba0d7c21c6ebb61f223ffa86e49c7022d2d564f9531e5d80e48d91277101780ff77d1c37ccd5e" + }, + "multiexp": null, + "crt": null + } + ] + } +} \ No newline at end of file diff --git a/tss/cggmp21/testdata/share-2.json b/tss/cggmp21/testdata/share-2.json new file mode 100644 index 00000000..4fe1dff1 --- /dev/null +++ b/tss/cggmp21/testdata/share-2.json @@ -0,0 +1,81 @@ +{ + "core": { + "curve": "secp256k1", + "i": 2, + "shared_public_key": "03910c0c6391247e94f9e359bea4b5f2630c90cc68124b056814a5d34e969449e2", + "public_shares": [ + "039bbf107d3d6c52543b23b17a235b18b9e27a00401c29d6dcfd8c36b2eae2af80", + "0354513c526919217c83d3d6364ed7bcf9a4d935caeae1bb4c954e661e3cc833c3", + "02e99ddc7b27efb95afb920f17cb80ac36e1817979caeb7256bc068eabb84a5a24" + ], + "vss_setup": { + "min_signers": 2, + "I": [ + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000003" + ] + }, + "x": "9d9ba6ea479ed5d3663dd64d564ed3c6fe6bc8874d2e50233bc169856ad3b259" + }, + "aux": { + "p": { + "radix": 16, + "value": "f7a7ad0bd2db4f8cbd490cb325957c728df9bd971f8989fde75c53aea9cbfca60b03356eceb01bfa3630d2758c4e8fd85922365125ea196599cc1ddf464ae2a3ff25259fc891afeac0094ef3a90b27e8758b37ffdfe85525d9a95198f8cba276e3da895865ae4a4c63c2d711b2c2004ec46b8ff82542eae257ef5c879796027f" + }, + "q": { + "radix": 16, + "value": "9c356bd966e859248f4f07c44340950285bfbde9fbc8b7ac6bcde91774bfd13795379c6a954d837cdbf11cfa7568eaa662420eb549a01cd0a770d8a94eb20422e73d79a9c5a42a5f67a07f75d9df6bdf079b979088c05adc5ceee2faa514d21d2d502d77f5b657e69904d0518bfb4bcb7293fc5f158ea11301e835a159449467" + }, + "parties": [ + { + "N": { + "radix": 16, + "value": "ba5beb56c1097ef06edf175d621fb72a69fb216ffdf38e322d544735ae4e8da17fc996bafa8ea820f963a0c3dd1d45cee5de123ec7e5a5b33e8e4f07a99ec447ca336c612dec67fd32b501d21032ce65b3a0e0073b7c9f4599caa92b5bb159131befa3dd2aab5d1f8778186f1c602a7c910f923d5f0ba5cf1009bc75c74085a432a290704065cf81cddadd3b014c8e660fde87bbe605fb23ec132566dd70fa431ba51b6cd39ba49fade14fe130930aedc9fdd6464cd0a79d2b99100617c695725dcc62fd03c0a02982e93f003b525854863b389452fc02bcf92b51523e0fd8d17fac9072093349a4bda9998a027e9370d5dc20c0f4532d44306a0e25f9765251" + }, + "s": { + "radix": 16, + "value": "57f1caf00ab4879c89ea92f1f206f56caee51a92772c20c939878480569ab20d273af03054f8fcc22aac46ebab1f3f9e8ebf39eb2616a5dc5a161b6e43c968da6fd4491df3c488013353e429e365a7e4a772bcfdf2e29a7701ef07a84a443e2532658ba27a5966ec44ec6d7103586e9af4ce6acca9f93b7d6a3dea9f79f6db4c92a442f5a862adcbcc245d4da353b0c0967353bcd6e10c19b67933ffb0ca5996115792215708f6c5205df76379b73d220102d8a200a6b16325501950c0422026ef2de268c2d3a169f274263aa2007423187f0b4ecbc5da8ad52653f56959c1c797f2d003f69f45578ee1164bb64b832fae569368c139ad908903d3a37d51704f" + }, + "t": { + "radix": 16, + "value": "7b49f5f7f8266a0e201570b0d41c83bc236bd9464b92762a7db8388d481da1872ee899da52d862aeb8ddd63fcd037496f16510caa239a2e04ee3703d19bdeb4a0eeb057697f6eca59e0b4a66cb1ea596a484644898d98552424d7e748d5f42ee8a07087a434cfd73db3c3f1a2678bcbe256377c60489529949a1251b25c47e19c318024bde5bb8755edbbad88609bb40806ad77d8e72ccabd49443f147381954d490898e99f8c8e984b242de5c0af5e7b439a9ccc015f3d704a17f146bee033bb71b12cbe57f22a8324f02af5eea0652388696c93f1096ee51bad4f608f8799d18d96ce51a94d529b2fac8efcc673f995b20823a26c943e9c05f1825a5cab96" + }, + "multiexp": null, + "crt": null + }, + { + "N": { + "radix": 16, + "value": "a7ec71b4ab2eea6b27abdd877a6ef15fe12ec9c2bddbdf50a500dc91ca1a35eb6346d96ac5d928280d5da83c6697a91512914aad7161732982855559e98eded81831d35ccc9ba01d445c9789c99c107cddbd516b7657d4a6769809a8cb5a76ebac559f4d4e0341ad8fb06538f8d7dc9378fed58e1ee796207c738f7ce6e3e558aa737648974b3a4a6b290b89d79e4928104d9d87b44d1e7da70ce0146496a15499959bd8d5e05f5f432846497047f060c976a6bf71f75230f1774c44741c82ff8eadd88278b8a84f9087817529fb811067fd5e34d8e4e4df428cd08c3dc85b5813869f00efa90d774a7606a66cee3fae15003552575cf3092c537e2fa28420c9" + }, + "s": { + "radix": 16, + "value": "427a2160b77d4596b4e4fcc7db745e21557fd69d051bc7a7e0830e06b363c975765c7db90992ab9943eba63860a2f8231471e2ad20d559b7ba274cbe5a1c75606f8bb24b1f12f167dc8044ddf261beaeb8a944001a70862e44d8c6e0ce8b870207c74fa5ab51d3869b0778fbaa1f49b20dbdb509b9fc15fdf7c72b63838005e7825973dbace607641774210293dfe10193858d8af831ee80115360756d2f0f0c8918f79ba74b35c1913e549f7e720afe9c2181c850e4a09b186d3018cd5b01ed8238c050496f8ada1d2907a00c7a0de91e9d3e5b7b01ba2d6654c7d0b172009b6e7b1b35cfa6ddcd169fdd495e91f6d92958c457bedca931b5cf0acc5be47853" + }, + "t": { + "radix": 16, + "value": "762f51c1d412541dd6a4553a85f44164c6b0221dc96c45c54114f4fded6246d6879fd3614f7980b89e3f12c46936e126a05174cfd46d10775894f7eb55d613fbd4490f72ab90f63608045f5a31df57acf4543adc693cc7c29d0dc192498b9280050bb8d2f4bd42f322d1c6ea2774b45fecaabf798ce71f28972cff4c761bd87d3987fe4eb15da36229892f209a7ae0f0032daf249d95897b4c2ca89726863e6b0370438f9bab99e0f7f5dd1a0fc2934e46879508c73ab2a82e73a3b23c2b4825ca7deb81e1a816f56beb1708090592aa6276055b167abda36e35db71ba5e601f127d849fe5d08bae8f6b1525e6084f780a1fc0c3315d7ae0c4f859b559a8f888" + }, + "multiexp": null, + "crt": null + }, + { + "N": { + "radix": 16, + "value": "971ddb7f6df3812cd3bcfd9c4c48067fccc88ec112b7789b62c4b1b0ef6b532b94afa0c70cae251280a56d4e4b7d366e995d126606e1a123540d6548cac9477bd515f8ae2a0be374be72f0d2f221a4ed557bdeff1d80b434782458eb7e6aa13da19de98a11f1b42d270cd44bdb30d91902ddd8f6773f14199522cc43bd82b76ebe5a281432d617728465fe55423bb8631f72799f7ce89ed124b050062d322171e1cc45e8ee0ff14b437ccdb064423059c9703871e7c050dd7f48fb9b253b47fdb387e6c00ed3c9444362e169a248868ed103343165a1f85bd652e5c20f84e5a33de696c27e0dd1b306d7ba56cfd936292d36399765b936b1a077e5c687886d19" + }, + "s": { + "radix": 16, + "value": "63a5bef6c9a145d96c7299859757a6e526c853eb60b7e129c807671f85f4041622ce5c3cfc248a75cff2d61384d7223d86a06a14e197fd1aede2e4abd23ed6c670a974353667fee740b4b4f6492defd6992b167a0008b325e87a94f4299696c21f0d0c3666592610c4a9d4c1250ecc6e781768bca404c488c0ff478dd26270a07b32f7843f1201c7b045d3ddfa8262ef3fceaedd0b8ddce3c867c08724f32714e99ccb2fa4a08b92fe87e857c9498b78f142db00c634aff1e211d6a84612352fc982e415898f0c50c3b70e36f5b3cac030c05e15330e1667a0f71d71da5650669053322fdef0521cb8904ee685e0d4caa6a215747d0af77dc58b4b15fef02c56" + }, + "t": { + "radix": 16, + "value": "1803b7469c8f3f6b7e60d0bac8a02b1cafd5e31bd5a448cd15a3e83951dc4963772caa8076a5ce5bf15a8c0bdbccdaf63d05613e9a3d349771186950daf2fd22c693d264901bdfc29aa768e81319cc440dd1b604672c733019d2fd8a3589b492a6dd2cd749df2179413cc1288afaab1f8bc9727272b79423780cb8bcf5fc1cf30c3822b680ce59a4648fa9ad556fad5f2eed98a8f55036e9d47c216aa3f9d9ec9ba9ceb1c988adc13887a254e5cc12ba7acc1fc7234cbcd797817dcf5d1a12865c8bf059b7d3b6748fe8c0abe1cf51a0ab94df2c1201fad711bba0d7c21c6ebb61f223ffa86e49c7022d2d564f9531e5d80e48d91277101780ff77d1c37ccd5e" + }, + "multiexp": null, + "crt": null + } + ] + } +} \ No newline at end of file diff --git a/tss/ecdsa/signing/signing.go b/tss/ecdsa/signing/signing.go index ea7fe6e3..5f1d5e31 100644 --- a/tss/ecdsa/signing/signing.go +++ b/tss/ecdsa/signing/signing.go @@ -6,25 +6,24 @@ package signing import ( "context" "encoding/json" + "errors" "fmt" "math/big" - "reflect" "slices" + "sort" "sync" "time" - tssCommon "github.com/binance-chain/tss-lib/common" - "github.com/binance-chain/tss-lib/ecdsa/signing" - "github.com/binance-chain/tss-lib/tss" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/sourcegraph/conc/pool" "github.com/sprintertech/sprinter-signing/comm" "github.com/sprintertech/sprinter-signing/keyshare" - errors "github.com/sprintertech/sprinter-signing/tss" + tsserrors "github.com/sprintertech/sprinter-signing/tss" + "github.com/sprintertech/sprinter-signing/tss/cggmp21" "github.com/sprintertech/sprinter-signing/tss/ecdsa/common" "github.com/sprintertech/sprinter-signing/tss/message" "github.com/sprintertech/sprinter-signing/tss/util" @@ -41,11 +40,26 @@ type EcdsaSignature struct { ID string } +// Signing drives a single cggmp21 threshold-ECDSA signing session for one +// party. It satisfies the tss.TssProcess interface; the underlying MPC is +// driven via tss/cggmp21 (Rust FFI), with this party-set/messaging plumbing +// kept in Go. type Signing struct { - common.BaseTss + host host.Host + communication comm.Communication + log zerolog.Logger + // TssTimeout bounds how long the protocol may stall waiting for a new + // inbound message before erroring out. Exported so tests can shorten it. + TssTimeout time.Duration + + sid string + msg *big.Int + key keyshare.ECDSAKeyshare + + mux sync.Mutex + started bool + cancel context.CancelFunc coordinator bool - key keyshare.ECDSAKeyshare - msg *big.Int resultChn chan interface{} subscriptionID comm.SubscriptionID } @@ -55,7 +69,7 @@ func NewSigning( messageID string, sessionID string, host host.Host, - comm comm.Communication, + communication comm.Communication, fetcher SaveDataFetcher, ) (*Signing, error) { fetcher.LockKeyshare() @@ -65,238 +79,290 @@ func NewSigning( return nil, err } - partyStore := make(map[string]*tss.PartyID) return &Signing{ - BaseTss: common.BaseTss{ - PartyStore: partyStore, - Host: host, - Communication: comm, - Peers: key.Peers, - SID: sessionID, - Started: false, - Mux: &sync.Mutex{}, - Log: log.With().Str("SessionID", sessionID).Str("messageID", messageID).Str("Process", "signing").Logger(), - Cancel: func() {}, - TssTimeout: time.Second * 8, - }, - key: key, - msg: msg, + host: host, + communication: communication, + sid: sessionID, + msg: msg, + key: key, + log: log.With().Str("SessionID", sessionID).Str("messageID", messageID).Str("Process", "signing").Logger(), + cancel: func() {}, + TssTimeout: 8 * time.Second, }, nil } -// Run initializes the signing party and runs the signing tss process. -// Params contains peer subset that leaders sends with start message. +// SessionID returns the signing session identifier. +func (s *Signing) SessionID() string { return s.sid } + +// Timeout returns the configured timeout for this signing process. +func (s *Signing) Timeout() time.Duration { return s.TssTimeout } + +// Retryable signals that signing can be safely retried on failure. +func (s *Signing) Retryable() bool { return true } + +// ValidCoordinators returns peers that hold a valid key share and can act as +// the signing coordinator for this process. +func (s *Signing) ValidCoordinators() []peer.ID { return s.key.Peers } + +// Ready reports whether enough peers are ready to start signing (threshold+1). +func (s *Signing) Ready(readyPeers []peer.ID, _ []peer.ID) (bool, error) { + return len(s.readyParticipants(readyPeers)) == s.key.Threshold+1, nil +} + +// StartParams chooses threshold+1 peers from the ready set deterministically +// (sorted by hash(peer || session)) and returns the JSON-encoded subset. +func (s *Signing) StartParams(readyPeers []peer.ID) []byte { + readyPeers = s.readyParticipants(readyPeers) + sorted := util.SortPeersForSession(readyPeers, s.sid) + subset := make([]peer.ID, 0, s.key.Threshold+1) + for _, p := range sorted { + subset = append(subset, p.ID) + if len(subset) == s.key.Threshold+1 { + break + } + } + b, _ := json.Marshal(subset) + return b +} + +// Stop tears down communication subscriptions and cancels the running protocol. +func (s *Signing) Stop() { + s.log.Info().Msgf("Stopping tss process.") + s.communication.UnSubscribe(s.subscriptionID) + s.cancel() +} + +// Run starts the signing process. params carries the coordinator-chosen peer +// subset that this party must use. func (s *Signing) Run( ctx context.Context, coordinator bool, resultChn chan interface{}, params []byte, ) error { - s.Mux.Lock() - if s.Started { - s.Mux.Unlock() - s.Log.Warn().Msgf("Signing already started") + s.mux.Lock() + if s.started { + s.mux.Unlock() + s.log.Warn().Msgf("Signing already started") return common.ErrProcessStarted } - s.Started = true - s.Mux.Unlock() + s.started = true + s.mux.Unlock() s.coordinator = coordinator s.resultChn = resultChn - ctx, s.Cancel = context.WithCancel(ctx) + ctx, s.cancel = context.WithCancel(ctx) - peerSubset, err := s.unmarshallStartParams(params) + // Subscribe FIRST — before any FFI work — so we never miss inbound messages + // from peers that may start signing slightly ahead of us. The buffered + // channel lets pre-Setup messages queue without blocking the sender. + msgChn := make(chan *comm.WrappedMessage, 64) + s.subscriptionID = s.communication.Subscribe(s.sid, comm.TssKeySignMsg, msgChn) + + signerPeers, err := unmarshallStartParams(params) if err != nil { return err } + if !util.IsParticipant(s.host.ID(), signerPeers) { + return &tsserrors.SubsetError{Peer: s.host.ID()} + } - if !util.IsParticipant(s.Host.ID(), peerSubset) { - return &errors.SubsetError{Peer: s.Host.ID()} + // Establish the deterministic peer ↔ index mappings shared by every signer. + signerPeers = sortPeersByPartyKey(signerPeers) + peerToSignIdx := make(map[peer.ID]uint16, len(signerPeers)) + for i, p := range signerPeers { + peerToSignIdx[p] = uint16(i) + } + keygenSigners := make([]uint16, len(signerPeers)) + for i, p := range signerPeers { + ki, err := peerKeygenIndex(p, s.key.Peers) + if err != nil { + return fmt.Errorf("signing: %w", err) + } + keygenSigners[i] = ki } + mySignIdx := peerToSignIdx[s.host.ID()] - s.Peers = peerSubset - parties := common.PartiesFromPeers(s.Peers) - s.PopulatePartyStore(parties) - pCtx := tss.NewPeerContext(parties) - tssParams, err := tss.NewParameters(tss.S256(), pCtx, s.PartyStore[s.Host.ID().String()], len(parties), s.key.Threshold) + shareJSON, err := s.key.CggmpShare() if err != nil { - return err + return fmt.Errorf("signing: convert keyshare to cggmp21: %w", err) } - sigChn := make(chan tssCommon.SignatureData) - outChn := make(chan tss.Message) - kdd := big.NewInt(0) - s.Party, err = signing.NewLocalParty( - s.msg, - tssParams, - s.key.Key, - kdd, - outChn, - sigChn, - new(big.Int).SetBytes([]byte(s.SID))) + hash := make([]byte, 32) + s.msg.FillBytes(hash) + + session, err := cggmp21.NewSigningSession(shareJSON, []byte(s.sid), mySignIdx, keygenSigners, hash) if err != nil { - return err + return fmt.Errorf("signing: %w", err) } + defer session.Close() - msgChn := make(chan *comm.WrappedMessage) - s.subscriptionID = s.Communication.Subscribe(s.SessionID(), comm.TssKeySignMsg, msgChn) + s.log.Info().Msgf("Started signing process for message %s", s.msg.Text(16)) p := pool.New().WithContext(ctx).WithCancelOnError() - p.Go(func(ctx context.Context) error { return s.ProcessOutboundMessages(ctx, outChn, comm.TssKeySignMsg) }) - p.Go(func(ctx context.Context) error { return s.ProcessInboundMessages(ctx, msgChn) }) - p.Go(func(ctx context.Context) error { return s.processEndMessage(ctx, sigChn) }) - p.Go(func(ctx context.Context) error { return s.monitorSigning(ctx) }) - - s.Log.Info().Msgf("Started signing process for message %s", s.msg.Text(16)) - - tssError := s.Party.Start() - if tssError != nil { - return tssError - } - + p.Go(func(ctx context.Context) error { + return s.drive(ctx, session, signerPeers, peerToSignIdx, msgChn) + }) return p.Wait() } -// Stop ends all subscriptions created when starting the tss process. -func (s *Signing) Stop() { - s.Log.Info().Msgf("Stopping tss process.") - s.Communication.UnSubscribe(s.subscriptionID) - s.Cancel() -} - -// Ready returns true if threshold+1 parties are ready to start the signing process. -func (s *Signing) Ready(readyPeers []peer.ID, excludedPeers []peer.ID) (bool, error) { - readyPeers = s.readyParticipants(readyPeers) - return len(readyPeers) == s.key.Threshold+1, nil -} - -// ValidCoordinators returns only peers that have a valid keyshare -func (s *Signing) ValidCoordinators() []peer.ID { - return s.key.Peers -} +// drive owns the cggmp21 session: drains outgoing messages, delivers inbound +// messages, and finalises the signature. Single-threaded by design — the +// session is not safe for concurrent use. +func (s *Signing) drive( + ctx context.Context, + session *cggmp21.SigningSession, + signerPeers []peer.ID, + peerToSignIdx map[peer.ID]uint16, + msgChn chan *comm.WrappedMessage, +) error { + defer s.cancel() -// StartParams returns peer subset for this tss process. It is calculated -// by sorting hashes of peer IDs and session ID and chosing ready peers alphabetically -// until threshold is satisfied. -func (s *Signing) StartParams(readyPeers []peer.ID) []byte { - readyPeers = s.readyParticipants(readyPeers) - peers := []peer.ID{} - peers = append(peers, readyPeers...) - - sortedPeers := util.SortPeersForSession(peers, s.SessionID()) - peerSubset := []peer.ID{} - for _, peer := range sortedPeers { - peerSubset = append(peerSubset, peer.ID) - if len(peerSubset) == s.key.Threshold+1 { - break + send := func() error { + for { + msg, ok, err := session.NextOutgoing() + if err != nil { + return err + } + if !ok { + return nil + } + wire, err := message.MarshalTssMessage(msg.Payload, msg.IsBroadcast()) + if err != nil { + s.log.Error().Err(err).Msg("marshal outgoing tss message") + continue + } + var dests []peer.ID + if msg.IsBroadcast() { + for _, p := range signerPeers { + if p != s.host.ID() { + dests = append(dests, p) + } + } + } else { + if int(msg.Recipient) >= len(signerPeers) { + return fmt.Errorf("signing: outgoing recipient %d out of range", msg.Recipient) + } + dests = []peer.ID{signerPeers[msg.Recipient]} + } + if err := s.communication.Broadcast(dests, wire, comm.TssKeySignMsg, s.sid); err != nil { + return fmt.Errorf("signing: broadcast: %w", err) + } } } - paramBytes, _ := json.Marshal(peerSubset) - return paramBytes -} - -func (s *Signing) unmarshallStartParams(paramBytes []byte) ([]peer.ID, error) { - var peerSubset []peer.ID - err := json.Unmarshal(paramBytes, &peerSubset) - if err != nil { - return []peer.ID{}, err + // Drain anything the session emitted at construction time. + if err := send(); err != nil { + return err } - return peerSubset, nil -} - -// processEndMessage routes signature to result channel. -func (s *Signing) processEndMessage(ctx context.Context, endChn chan tssCommon.SignatureData) error { - defer s.Cancel() for { - select { - //nolint - case sig := <-endChn: - { - s.Log.Info().Msg("Successfully generated signature") - - es := []byte{} - es = append(es[:], ethCommon.LeftPadBytes(sig.R, 32)...) - es = append(es[:], ethCommon.LeftPadBytes(sig.S, 32)...) - es = append(es[:], sig.SignatureRecovery...) - es[len(es)-1] += 27 // Transform V from 0/1 to 27/28 - - s.resultChn <- EcdsaSignature{ - Signature: es, - ID: s.SID, - } - - err := s.distributeSignature(es) - if err != nil { - log.Warn().Msgf("Failed distributing signature: %s", err) - } - return nil + if session.Done() { + sig, err := session.Signature() + if err != nil { + return fmt.Errorf("signing: %w", err) } + s.log.Info().Msg("Successfully generated signature") + s.resultChn <- EcdsaSignature{Signature: sig, ID: s.sid} + return s.distributeSignature(sig) + } + + select { case <-ctx.Done(): - { - return nil + return nil + case <-time.After(s.TssTimeout): + return &comm.CommunicationError{ + Err: fmt.Errorf("signing: timed out waiting for messages after %s", s.TssTimeout), + } + case wMsg := <-msgChn: + tssMsg, err := message.UnmarshalTssMessage(wMsg.Payload) + if err != nil { + s.log.Error().Err(err).Msgf("unmarshal message from %s", wMsg.From) + continue + } + senderIdx, ok := peerToSignIdx[wMsg.From] + if !ok { + s.log.Warn().Msgf("dropping message from non-signer %s", wMsg.From) + continue + } + if err := session.Deliver(senderIdx, tssMsg.IsBroadcast, tssMsg.MsgBytes); err != nil { + return fmt.Errorf("signing: deliver from %s: %w", wMsg.From, err) + } + if err := send(); err != nil { + return err } } } } -// readyParticipants returns all ready peers that contain a valid key share +// readyParticipants returns the subset of readyPeers that also hold a valid +// share (i.e. participated in keygen). func (s *Signing) readyParticipants(readyPeers []peer.ID) []peer.ID { - readyParticipants := make([]peer.ID, 0) - for _, peer := range readyPeers { - if !slices.Contains(s.key.Peers, peer) { - continue - } - - readyParticipants = append(readyParticipants, peer) - } - - return readyParticipants -} - -func (s *Signing) Retryable() bool { - return true -} - -// monitorSigning checks if the process is stuck and waiting for peers and sends an error -// if it is -func (s *Signing) monitorSigning(ctx context.Context) error { - defer s.Cancel() - waitingFor := make([]*tss.PartyID, 0) - ticker := time.NewTicker(time.Minute * 3) - - for { - select { - case <-ticker.C: - { - if len(waitingFor) != 0 && reflect.DeepEqual(s.Party.WaitingFor(), waitingFor) { - err := &comm.CommunicationError{ - Err: fmt.Errorf("waiting for peers %s", waitingFor), - } - return err - } - - waitingFor = s.Party.WaitingFor() - } - case <-ctx.Done(): - { - return nil - } + out := make([]peer.ID, 0, len(readyPeers)) + for _, p := range readyPeers { + if slices.Contains(s.key.Peers, p) { + out = append(out, p) } } + return out } +// distributeSignature broadcasts the final signature to peers that aren't part +// of the signing subset (so they learn the result). Non-coordinators skip; the +// coordinator handles result distribution. func (s *Signing) distributeSignature(sig []byte) error { if s.coordinator { return nil } - - sigMsg, err := message.MarshalSignatureMessage(s.SessionID(), sig) + sigMsg, err := message.MarshalSignatureMessage(s.sid, sig) if err != nil { return err } + return s.communication.Broadcast(s.host.Peerstore().Peers(), sigMsg, comm.SignatureMsg, comm.SignatureSessionID) +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +func unmarshallStartParams(b []byte) ([]peer.ID, error) { + var peers []peer.ID + if err := json.Unmarshal(b, &peers); err != nil { + return nil, err + } + if len(peers) == 0 { + return nil, errors.New("signing: empty peer subset in start params") + } + return peers, nil +} - err = s.Communication.Broadcast(s.Host.Peerstore().Peers(), sigMsg, comm.SignatureMsg, comm.SignatureSessionID) - return err +// partyKey returns the big.Int derived from peer.ID's bytes that tss-lib / +// threshlib uses for ordering parties (see CreatePartyID in +// tss/ecdsa/common/utils.go). The keygen-time index is the position of the +// peer in the sort order produced by these keys. +func partyKey(p peer.ID) *big.Int { + return new(big.Int).SetBytes([]byte(p.String())) +} + +// sortPeersByPartyKey returns peers sorted ascending by their party key, which +// is the canonical signing-time ordering used to map peers to signing indexes. +func sortPeersByPartyKey(peers []peer.ID) []peer.ID { + out := make([]peer.ID, len(peers)) + copy(out, peers) + sort.Slice(out, func(i, j int) bool { + return partyKey(out[i]).Cmp(partyKey(out[j])) < 0 + }) + return out +} + +// peerKeygenIndex returns the keygen-time index of `target` within the full +// set of keygen peers — i.e. its position in the sorted-by-party-key list of +// all peers that participated in keygen. cggmp21 needs this to look up the +// corresponding VSS evaluation point and public share for each signer. +func peerKeygenIndex(target peer.ID, keygenPeers []peer.ID) (uint16, error) { + sorted := sortPeersByPartyKey(keygenPeers) + for i, p := range sorted { + if p == target { + return uint16(i), nil + } + } + return 0, fmt.Errorf("peer %s not found among keygen peers", target) }