From a1b3e1298463aaedf871271a47edb2f216f5b500 Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 19 Feb 2026 14:47:35 -0600 Subject: [PATCH 01/96] Add FROST migration scaffold package and RFC --- ...c-20-schnorr-frost-migration-scaffold.adoc | 49 ++++++++++++++ pkg/frost/types.go | 58 +++++++++++++++++ pkg/frost/types_test.go | 65 +++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc create mode 100644 pkg/frost/types.go create mode 100644 pkg/frost/types_test.go diff --git a/docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc b/docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc new file mode 100644 index 0000000000..9b71288685 --- /dev/null +++ b/docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc @@ -0,0 +1,49 @@ += RFC-20: Schnorr/FROST Migration Scaffold + +*Author:* keep-core contributors +*Status:* Draft +*Date:* 2026-02-19 + +== Summary + +This RFC introduces the initial keep-core scaffolding for migrating tBTC from +threshold ECDSA signatures to Schnorr/FROST signatures. + +This change does not switch runtime signing logic yet. It defines core data +types and compatibility helpers required by follow-up protocol, chain, and +wallet orchestration changes. + +== Initial Deliverables + +* New `pkg/frost` package with: +** Taproot x-only output key type (`OutputKey`) +** BIP-340 Schnorr signature type (`Signature`) +** Serialization and logging helpers for Schnorr signatures +** Legacy compatibility alias helper: +`HASH160(0x02 || xOnlyOutputKey)` + +== Compatibility Model + +FROST wallets are expected to use 32-byte x-only keys as canonical identifiers. +During migration, legacy 20-byte wallet key hash paths are supported via +compatibility alias: + +---- +walletPubKeyHashCompat = HASH160(0x02 || xOnlyOutputKey) +---- + +== Follow-up Work + +1. Add FROST signer and coordinator interfaces to replace `pkg/tecdsa/signing`. +2. Introduce FROST DKG executor replacing GG18 pre-params and DKG wiring. +3. Update tBTC chain interfaces and wallet registry integration to accept + x-only keys as canonical wallet identities. +4. Update Bitcoin transaction builders to support P2TR key-path spends. +5. Add dual-stack runtime routing: GG18 existing wallets + FROST new wallets. +6. Add full integration tests for mixed wallet generations and migration flows. + +== Non-Goals (This RFC Revision) + +* No production FROST coordinator implementation. +* No on-chain contract ABI migration in this repository. +* No replacement of existing GG18 runtime paths yet. diff --git a/pkg/frost/types.go b/pkg/frost/types.go new file mode 100644 index 0000000000..4e25799916 --- /dev/null +++ b/pkg/frost/types.go @@ -0,0 +1,58 @@ +package frost + +import ( + "encoding/hex" + "fmt" + + "github.com/btcsuite/btcutil" +) + +const ( + // OutputKeySize is the byte length of a Taproot x-only output key. + OutputKeySize = 32 + // SignatureComponentSize is the byte length of each Schnorr signature part. + SignatureComponentSize = 32 +) + +// OutputKey is a Taproot x-only output key used by BIP-340/341. +type OutputKey [OutputKeySize]byte + +// WalletPublicKeyHashCompatibilityAlias computes the 20-byte compatibility +// alias from a Taproot output key: +// HASH160(0x02 || xOnlyOutputKey). +func WalletPublicKeyHashCompatibilityAlias(outputKey OutputKey) [20]byte { + serialized := make([]byte, 0, 1+OutputKeySize) + serialized = append(serialized, byte(0x02)) + serialized = append(serialized, outputKey[:]...) + + hash := btcutil.Hash160(serialized) + + var result [20]byte + copy(result[:], hash) + + return result +} + +// Signature is a 64-byte BIP-340 Schnorr signature split into its two +// 32-byte components: R (x-coordinate nonce commitment) and S (scalar). +type Signature struct { + R [SignatureComponentSize]byte + S [SignatureComponentSize]byte +} + +// Serialize concatenates signature components into a 64-byte value. +func (s *Signature) Serialize() [2 * SignatureComponentSize]byte { + var result [2 * SignatureComponentSize]byte + copy(result[0:SignatureComponentSize], s.R[:]) + copy(result[SignatureComponentSize:], s.S[:]) + return result +} + +// String returns a hex representation useful in logs. +func (s *Signature) String() string { + serialized := s.Serialize() + return fmt.Sprintf("R: 0x%s, S: 0x%s", + hex.EncodeToString(serialized[0:SignatureComponentSize]), + hex.EncodeToString(serialized[SignatureComponentSize:]), + ) +} diff --git a/pkg/frost/types_test.go b/pkg/frost/types_test.go new file mode 100644 index 0000000000..6f603565a7 --- /dev/null +++ b/pkg/frost/types_test.go @@ -0,0 +1,65 @@ +package frost + +import ( + "encoding/hex" + "testing" +) + +func TestWalletPublicKeyHashCompatibilityAlias(t *testing.T) { + outputKeyHex := "11223344556677889900aabbccddeeff00112233445566778899aabbccddeeff" + expectedAliasHex := "c2a27a88d8d03e271e8edc556923e9398619f17c" + + outputKeyBytes, err := hex.DecodeString(outputKeyHex) + if err != nil { + t.Fatalf("failed to decode output key: [%v]", err) + } + + var outputKey OutputKey + copy(outputKey[:], outputKeyBytes) + + actualAlias := WalletPublicKeyHashCompatibilityAlias(outputKey) + actualAliasHex := hex.EncodeToString(actualAlias[:]) + + if actualAliasHex != expectedAliasHex { + t.Fatalf( + "unexpected alias\nactual: [%s]\nexpected: [%s]", + actualAliasHex, + expectedAliasHex, + ) + } +} + +func TestSignatureSerialize(t *testing.T) { + signature := &Signature{} + signature.R = [SignatureComponentSize]byte{0x01, 0x02, 0x03} + signature.S = [SignatureComponentSize]byte{0xaa, 0xbb, 0xcc} + + serialized := signature.Serialize() + + if serialized[0] != 0x01 || serialized[1] != 0x02 || serialized[2] != 0x03 { + t.Fatalf("unexpected R serialization") + } + + if serialized[SignatureComponentSize] != 0xaa || + serialized[SignatureComponentSize+1] != 0xbb || + serialized[SignatureComponentSize+2] != 0xcc { + t.Fatalf("unexpected S serialization") + } +} + +func TestSignatureString(t *testing.T) { + signature := &Signature{ + R: [SignatureComponentSize]byte{0x01, 0x02}, + S: [SignatureComponentSize]byte{0x0a, 0x0b}, + } + + expected := "R: 0x0102000000000000000000000000000000000000000000000000000000000000, S: 0x0a0b000000000000000000000000000000000000000000000000000000000000" + + if signature.String() != expected { + t.Fatalf( + "unexpected signature string\nactual: [%s]\nexpected: [%s]", + signature.String(), + expected, + ) + } +} From 2d0a5c8c05f7642fb2e93333d4ada0f6fe8c7259 Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 19 Feb 2026 14:55:54 -0600 Subject: [PATCH 02/96] Set Schnorr/FROST RFC author to Threshold Labs --- docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc b/docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc index 9b71288685..7120c2d9c9 100644 --- a/docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc +++ b/docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc @@ -1,6 +1,6 @@ = RFC-20: Schnorr/FROST Migration Scaffold -*Author:* keep-core contributors +*Author:* Threshold Labs *Status:* Draft *Date:* 2026-02-19 From fc58fed960e7f9a3f64608e3d1051f232c2cc4c2 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 09:01:31 -0600 Subject: [PATCH 03/96] Wire tbtc runtime signing flow to frost signature types --- pkg/frost/signing/result.go | 9 ++ pkg/frost/signing/signing.go | 110 ++++++++++++++++++++++++ pkg/frost/signing/signing_test.go | 77 +++++++++++++++++ pkg/frost/types.go | 37 +++++++- pkg/frost/types_test.go | 29 +++++++ pkg/tbtc/coordination_test.go | 9 +- pkg/tbtc/deposit_sweep_test.go | 20 ++--- pkg/tbtc/heartbeat.go | 4 +- pkg/tbtc/heartbeat_test.go | 6 +- pkg/tbtc/marshaling.go | 3 +- pkg/tbtc/marshaling_test.go | 12 +-- pkg/tbtc/moved_funds_sweep_test.go | 20 ++--- pkg/tbtc/moving_funds_test.go | 19 ++-- pkg/tbtc/node.go | 2 +- pkg/tbtc/redemption_test.go | 20 ++--- pkg/tbtc/signature_test_helpers_test.go | 23 +++++ pkg/tbtc/signing.go | 12 +-- pkg/tbtc/signing_done.go | 8 +- pkg/tbtc/signing_done_test.go | 27 ++---- pkg/tbtc/signing_loop.go | 2 +- pkg/tbtc/signing_loop_test.go | 9 +- pkg/tbtc/signing_test.go | 8 +- pkg/tbtc/wallet.go | 7 +- pkg/tbtc/wallet_test.go | 13 +-- 24 files changed, 367 insertions(+), 119 deletions(-) create mode 100644 pkg/frost/signing/result.go create mode 100644 pkg/frost/signing/signing.go create mode 100644 pkg/frost/signing/signing_test.go create mode 100644 pkg/tbtc/signature_test_helpers_test.go diff --git a/pkg/frost/signing/result.go b/pkg/frost/signing/result.go new file mode 100644 index 0000000000..e02583057f --- /dev/null +++ b/pkg/frost/signing/result.go @@ -0,0 +1,9 @@ +package signing + +import "github.com/keep-network/keep-core/pkg/frost" + +// Result of the FROST signing protocol. +type Result struct { + // Signature is the BIP-340-style signature produced as result of signing. + Signature *frost.Signature +} diff --git a/pkg/frost/signing/signing.go b/pkg/frost/signing/signing.go new file mode 100644 index 0000000000..40f03acf62 --- /dev/null +++ b/pkg/frost/signing/signing.go @@ -0,0 +1,110 @@ +package signing + +import ( + "context" + "fmt" + "math/big" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tecdsa" + legacySigning "github.com/keep-network/keep-core/pkg/tecdsa/signing" +) + +// Execute runs signing and returns a Schnorr-shaped 64-byte signature. +// +// Transitional note: +// This implementation currently delegates group coordination and cryptographic +// operations to the legacy tECDSA engine and converts the resulting (R, S) +// components to the fixed-width Schnorr signature container. +func Execute( + ctx context.Context, + logger log.StandardLogger, + message *big.Int, + sessionID string, + memberIndex group.MemberIndex, + privateKeyShare *tecdsa.PrivateKeyShare, + groupSize int, + dishonestThreshold int, + excludedMembersIndexes []group.MemberIndex, + channel net.BroadcastChannel, + membershipValidator *group.MembershipValidator, +) (*Result, error) { + legacyResult, err := legacySigning.Execute( + ctx, + logger, + message, + sessionID, + memberIndex, + privateKeyShare, + groupSize, + dishonestThreshold, + excludedMembersIndexes, + channel, + membershipValidator, + ) + if err != nil { + return nil, err + } + + signature, err := FromTECDSASignature(legacyResult.Signature) + if err != nil { + return nil, err + } + + return &Result{Signature: signature}, nil +} + +// RegisterUnmarshallers initializes all required message unmarshallers. +// For now, signing transport message formats are delegated to the legacy +// engine implementation. +func RegisterUnmarshallers(channel net.BroadcastChannel) { + legacySigning.RegisterUnmarshallers(channel) +} + +// FromTECDSASignature maps a legacy signature to the fixed-width Schnorr +// signature container by preserving R/S values and dropping RecoveryID. +func FromTECDSASignature(signature *tecdsa.Signature) (*frost.Signature, error) { + if signature == nil { + return nil, fmt.Errorf("signature is nil") + } + + if signature.R == nil || signature.S == nil { + return nil, fmt.Errorf("signature components cannot be nil") + } + + if signature.R.Sign() < 0 || signature.S.Sign() < 0 { + return nil, fmt.Errorf("signature components cannot be negative") + } + + rBytes := signature.R.Bytes() + sBytes := signature.S.Bytes() + + if len(rBytes) > frost.SignatureComponentSize { + return nil, fmt.Errorf( + "R component too large: [%d] bytes", + len(rBytes), + ) + } + + if len(sBytes) > frost.SignatureComponentSize { + return nil, fmt.Errorf( + "S component too large: [%d] bytes", + len(sBytes), + ) + } + + frostSignature := &frost.Signature{} + copy( + frostSignature.R[frost.SignatureComponentSize-len(rBytes):], + rBytes, + ) + copy( + frostSignature.S[frost.SignatureComponentSize-len(sBytes):], + sBytes, + ) + + return frostSignature, nil +} diff --git a/pkg/frost/signing/signing_test.go b/pkg/frost/signing/signing_test.go new file mode 100644 index 0000000000..e0c3bc7b25 --- /dev/null +++ b/pkg/frost/signing/signing_test.go @@ -0,0 +1,77 @@ +package signing + +import ( + "math/big" + "testing" + + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestFromTECDSASignature(t *testing.T) { + signature := &tecdsa.Signature{ + R: big.NewInt(0x1234), + S: big.NewInt(0xabcd), + } + + result, err := FromTECDSASignature(signature) + if err != nil { + t.Fatalf("conversion failed: [%v]", err) + } + + if result.R[30] != 0x12 || result.R[31] != 0x34 { + t.Fatalf("unexpected R component bytes") + } + + if result.S[30] != 0xab || result.S[31] != 0xcd { + t.Fatalf("unexpected S component bytes") + } +} + +func TestFromTECDSASignature_ValidationErrors(t *testing.T) { + testData := []struct { + name string + signature *tecdsa.Signature + }{ + { + name: "nil signature", + signature: nil, + }, + { + name: "nil R", + signature: &tecdsa.Signature{ + R: nil, + S: big.NewInt(1), + }, + }, + { + name: "nil S", + signature: &tecdsa.Signature{ + R: big.NewInt(1), + S: nil, + }, + }, + { + name: "negative R", + signature: &tecdsa.Signature{ + R: big.NewInt(-1), + S: big.NewInt(1), + }, + }, + { + name: "negative S", + signature: &tecdsa.Signature{ + R: big.NewInt(1), + S: big.NewInt(-1), + }, + }, + } + + for _, tc := range testData { + t.Run(tc.name, func(t *testing.T) { + _, err := FromTECDSASignature(tc.signature) + if err == nil { + t.Fatal("expected conversion error") + } + }) + } +} diff --git a/pkg/frost/types.go b/pkg/frost/types.go index 4e25799916..f1f4b0f069 100644 --- a/pkg/frost/types.go +++ b/pkg/frost/types.go @@ -12,6 +12,8 @@ const ( OutputKeySize = 32 // SignatureComponentSize is the byte length of each Schnorr signature part. SignatureComponentSize = 32 + // SignatureSize is the full serialized BIP-340 signature length. + SignatureSize = 2 * SignatureComponentSize ) // OutputKey is a Taproot x-only output key used by BIP-340/341. @@ -42,12 +44,45 @@ type Signature struct { // Serialize concatenates signature components into a 64-byte value. func (s *Signature) Serialize() [2 * SignatureComponentSize]byte { - var result [2 * SignatureComponentSize]byte + var result [SignatureSize]byte copy(result[0:SignatureComponentSize], s.R[:]) copy(result[SignatureComponentSize:], s.S[:]) return result } +// Marshal encodes signature into a 64-byte canonical form. +func (s *Signature) Marshal() ([]byte, error) { + serialized := s.Serialize() + result := make([]byte, SignatureSize) + copy(result, serialized[:]) + return result, nil +} + +// Unmarshal decodes signature from a 64-byte canonical form. +func (s *Signature) Unmarshal(data []byte) error { + if len(data) != SignatureSize { + return fmt.Errorf( + "invalid signature length: [%d], expected [%d]", + len(data), + SignatureSize, + ) + } + + copy(s.R[:], data[:SignatureComponentSize]) + copy(s.S[:], data[SignatureComponentSize:]) + + return nil +} + +// Equals determines whether two signatures are equal. +func (s *Signature) Equals(other *Signature) bool { + if s == nil || other == nil { + return s == other + } + + return s.R == other.R && s.S == other.S +} + // String returns a hex representation useful in logs. func (s *Signature) String() string { serialized := s.Serialize() diff --git a/pkg/frost/types_test.go b/pkg/frost/types_test.go index 6f603565a7..ef3cbdb520 100644 --- a/pkg/frost/types_test.go +++ b/pkg/frost/types_test.go @@ -47,6 +47,35 @@ func TestSignatureSerialize(t *testing.T) { } } +func TestSignatureMarshalUnmarshal(t *testing.T) { + original := &Signature{ + R: [SignatureComponentSize]byte{0x11, 0x22, 0x33}, + S: [SignatureComponentSize]byte{0xaa, 0xbb, 0xcc}, + } + + marshaled, err := original.Marshal() + if err != nil { + t.Fatalf("marshal failed: [%v]", err) + } + + decoded := &Signature{} + if err := decoded.Unmarshal(marshaled); err != nil { + t.Fatalf("unmarshal failed: [%v]", err) + } + + if !original.Equals(decoded) { + t.Fatalf("decoded signature does not match original") + } +} + +func TestSignatureUnmarshal_InvalidLength(t *testing.T) { + signature := &Signature{} + err := signature.Unmarshal([]byte{0x01, 0x02, 0x03}) + if err == nil { + t.Fatal("expected invalid-length unmarshal error") + } +} + func TestSignatureString(t *testing.T) { signature := &Signature{ R: [SignatureComponentSize]byte{0x01, 0x02}, diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index de9e0b4df8..b61da6e7f7 100644 --- a/pkg/tbtc/coordination_test.go +++ b/pkg/tbtc/coordination_test.go @@ -19,7 +19,6 @@ import ( netlocal "github.com/keep-network/keep-core/pkg/net/local" "github.com/keep-network/keep-core/pkg/operator" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa" "golang.org/x/exp/slices" "github.com/keep-network/keep-core/internal/testutils" @@ -1034,12 +1033,8 @@ func TestCoordinationExecutor_ExecuteFollowerRoutine(t *testing.T) { senderID: leaderID, message: big.NewInt(100), attemptNumber: 2, - signature: &tecdsa.Signature{ - R: big.NewInt(200), - S: big.NewInt(300), - RecoveryID: 3, - }, - endBlock: 4500, + signature: mustFrostSignatureFromBigInts(big.NewInt(200), big.NewInt(300)), + endBlock: 4500, }) if err != nil { t.Error(err) diff --git a/pkg/tbtc/deposit_sweep_test.go b/pkg/tbtc/deposit_sweep_test.go index c98f75a3c0..08a2c83eaf 100644 --- a/pkg/tbtc/deposit_sweep_test.go +++ b/pkg/tbtc/deposit_sweep_test.go @@ -7,10 +7,9 @@ import ( "testing" "time" - "github.com/keep-network/keep-core/pkg/tecdsa" - "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/tbtc/internal/test" ) @@ -171,16 +170,15 @@ func TestDepositSweepAction_Execute(t *testing.T) { // Create a signing executor mock instance. signingExecutor := newMockWalletSigningExecutor() - // The signatures within the scenario fixture are in the format - // suitable for applying them directly to a Bitcoin transaction. - // However, the signing executor operates on raw tECDSA signatures - // so, we need to unpack them first. - rawSignatures := make([]*tecdsa.Signature, len(scenario.Signatures)) + // The signatures within the scenario fixture are represented as + // big integer components and need conversion to runtime signature + // containers used by signing executor. + rawSignatures := make([]*frost.Signature, len(scenario.Signatures)) for i, signature := range scenario.Signatures { - rawSignatures[i] = &tecdsa.Signature{ - R: signature.R, - S: signature.S, - } + rawSignatures[i] = mustFrostSignatureFromBigInts( + signature.R, + signature.S, + ) } // Set up the signing executor mock to return the signatures from diff --git a/pkg/tbtc/heartbeat.go b/pkg/tbtc/heartbeat.go index c86afd88db..64fad1556e 100644 --- a/pkg/tbtc/heartbeat.go +++ b/pkg/tbtc/heartbeat.go @@ -9,8 +9,8 @@ import ( "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa" ) const ( @@ -60,7 +60,7 @@ type heartbeatSigningExecutor interface { ctx context.Context, message *big.Int, startBlock uint64, - ) (*tecdsa.Signature, *signingActivityReport, uint64, error) + ) (*frost.Signature, *signingActivityReport, uint64, error) } // heartbeatInactivityClaimExecutor is an interface meant to decouple the diff --git a/pkg/tbtc/heartbeat_test.go b/pkg/tbtc/heartbeat_test.go index c635659a08..7d833f16ab 100644 --- a/pkg/tbtc/heartbeat_test.go +++ b/pkg/tbtc/heartbeat_test.go @@ -10,8 +10,8 @@ import ( "testing" "github.com/keep-network/keep-core/internal/testutils" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa" ) func TestHeartbeatAction_HappyPath(t *testing.T) { @@ -612,7 +612,7 @@ func (mhse *mockHeartbeatSigningExecutor) sign( ctx context.Context, message *big.Int, startBlock uint64, -) (*tecdsa.Signature, *signingActivityReport, uint64, error) { +) (*frost.Signature, *signingActivityReport, uint64, error) { mhse.requestedMessage = message mhse.requestedStartBlock = startBlock @@ -636,7 +636,7 @@ func (mhse *mockHeartbeatSigningExecutor) sign( inactiveMembers: inactiveMembers, } - return &tecdsa.Signature{}, activityReport, startBlock + 1, nil + return &frost.Signature{}, activityReport, startBlock + 1, nil } type mockInactivityClaimExecutor struct { diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index 3ee310d21b..f96be4f1c1 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -12,6 +12,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tbtc/gen/pb" "github.com/keep-network/keep-core/pkg/tecdsa" @@ -114,7 +115,7 @@ func (sdm *signingDoneMessage) Unmarshal(bytes []byte) error { return err } - signature := &tecdsa.Signature{} + signature := &frost.Signature{} if err := signature.Unmarshal(pbMsg.Signature); err != nil { return fmt.Errorf("cannot unmarshal signature: [%v]", err) } diff --git a/pkg/tbtc/marshaling_test.go b/pkg/tbtc/marshaling_test.go index 892d234ecc..57deeb01c4 100644 --- a/pkg/tbtc/marshaling_test.go +++ b/pkg/tbtc/marshaling_test.go @@ -13,9 +13,9 @@ import ( fuzz "github.com/google/gofuzz" "github.com/keep-network/keep-core/internal/testutils" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/internal/pbutils" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa" ) func TestSignerMarshalling(t *testing.T) { @@ -53,12 +53,8 @@ func TestSigningDoneMessage_MarshalingRoundtrip(t *testing.T) { senderID: group.MemberIndex(10), message: big.NewInt(100), attemptNumber: 2, - signature: &tecdsa.Signature{ - R: big.NewInt(200), - S: big.NewInt(300), - RecoveryID: 3, - }, - endBlock: 4500, + signature: mustFrostSignatureFromBigInts(big.NewInt(200), big.NewInt(300)), + endBlock: 4500, } unmarshaled := &signingDoneMessage{} @@ -78,7 +74,7 @@ func TestFuzzSigningDoneMessage_MarshalingRoundtrip(t *testing.T) { senderID group.MemberIndex message big.Int attemptNumber uint64 - signature tecdsa.Signature + signature frost.Signature endBlock uint64 ) diff --git a/pkg/tbtc/moved_funds_sweep_test.go b/pkg/tbtc/moved_funds_sweep_test.go index 68ae7be032..76119a16d5 100644 --- a/pkg/tbtc/moved_funds_sweep_test.go +++ b/pkg/tbtc/moved_funds_sweep_test.go @@ -7,10 +7,9 @@ import ( "testing" "time" - "github.com/keep-network/keep-core/pkg/tecdsa" - "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/tbtc/internal/test" ) @@ -78,16 +77,15 @@ func TestMovedFundsSweepAction_Execute(t *testing.T) { // Create a signing executor mock instance. signingExecutor := newMockWalletSigningExecutor() - // The signatures within the scenario fixture are in the format - // suitable for applying them directly to a Bitcoin transaction. - // However, the signing executor operates on raw tECDSA signatures - // so, we need to unpack them first. - rawSignatures := make([]*tecdsa.Signature, len(scenario.Signatures)) + // The signatures within the scenario fixture are represented as + // big integer components and need conversion to runtime signature + // containers used by signing executor. + rawSignatures := make([]*frost.Signature, len(scenario.Signatures)) for i, signature := range scenario.Signatures { - rawSignatures[i] = &tecdsa.Signature{ - R: signature.R, - S: signature.S, - } + rawSignatures[i] = mustFrostSignatureFromBigInts( + signature.R, + signature.S, + ) } // Set up the signing executor mock to return the signatures from diff --git a/pkg/tbtc/moving_funds_test.go b/pkg/tbtc/moving_funds_test.go index d1fb2b99d4..42134aec60 100644 --- a/pkg/tbtc/moving_funds_test.go +++ b/pkg/tbtc/moving_funds_test.go @@ -11,8 +11,8 @@ import ( "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/tbtc/internal/test" - "github.com/keep-network/keep-core/pkg/tecdsa" ) // TODO: Think about covering unhappy paths for specific steps of the moving funds action. @@ -92,14 +92,13 @@ func TestMovingFundsAction_Execute(t *testing.T) { // Create a signing executor mock instance. signingExecutor := newMockWalletSigningExecutor() - // The signature within the scenario fixture is in the format - // suitable for applying them directly to a Bitcoin transaction. - // However, the signing executor operates on raw tECDSA signatures - // so, we need to unpack it first. - rawSignature := &tecdsa.Signature{ - R: scenario.Signature.R, - S: scenario.Signature.S, - } + // The signature within the scenario fixture is represented as + // big integer components and needs conversion to runtime signature + // container used by signing executor. + rawSignature := mustFrostSignatureFromBigInts( + scenario.Signature.R, + scenario.Signature.S, + ) // Set up the signing executor mock to return the signature from // the test fixture when called with the expected parameters. @@ -108,7 +107,7 @@ func TestMovingFundsAction_Execute(t *testing.T) { signingExecutor.setSignatures( []*big.Int{scenario.ExpectedSigHash}, proposalProcessingStartBlock+movingFundsCommitmentConfirmationBlocks, - []*tecdsa.Signature{rawSignature}, + []*frost.Signature{rawSignature}, ) action := newMovingFundsAction( diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 9c65a2a749..39023ab165 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -17,12 +17,12 @@ import ( "go.uber.org/zap" "github.com/keep-network/keep-common/pkg/persistence" + "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/announcer" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/protocol/inactivity" - "github.com/keep-network/keep-core/pkg/tecdsa/signing" ) const ( diff --git a/pkg/tbtc/redemption_test.go b/pkg/tbtc/redemption_test.go index 0a6897dd94..b2c35b8cb9 100644 --- a/pkg/tbtc/redemption_test.go +++ b/pkg/tbtc/redemption_test.go @@ -6,12 +6,11 @@ import ( "testing" "time" - "github.com/keep-network/keep-core/pkg/tecdsa" - "github.com/go-test/deep" "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/tbtc/internal/test" ) @@ -104,14 +103,13 @@ func TestRedemptionAction_Execute(t *testing.T) { // Create a signing executor mock instance. signingExecutor := newMockWalletSigningExecutor() - // The signature within the scenario fixture is in the format - // suitable for applying them directly to a Bitcoin transaction. - // However, the signing executor operates on raw tECDSA signatures - // so, we need to unpack it first. - rawSignature := &tecdsa.Signature{ - R: scenario.Signature.R, - S: scenario.Signature.S, - } + // The signature within the scenario fixture is represented as + // big integer components and needs conversion to runtime signature + // container used by signing executor. + rawSignature := mustFrostSignatureFromBigInts( + scenario.Signature.R, + scenario.Signature.S, + ) // Set up the signing executor mock to return the signature from // the test fixture when called with the expected parameters. @@ -120,7 +118,7 @@ func TestRedemptionAction_Execute(t *testing.T) { signingExecutor.setSignatures( []*big.Int{scenario.ExpectedSigHash}, proposalProcessingStartBlock, - []*tecdsa.Signature{rawSignature}, + []*frost.Signature{rawSignature}, ) action := newRedemptionAction( diff --git a/pkg/tbtc/signature_test_helpers_test.go b/pkg/tbtc/signature_test_helpers_test.go new file mode 100644 index 0000000000..b4019893d0 --- /dev/null +++ b/pkg/tbtc/signature_test_helpers_test.go @@ -0,0 +1,23 @@ +package tbtc + +import ( + "fmt" + "math/big" + + "github.com/keep-network/keep-core/pkg/frost" + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func mustFrostSignatureFromBigInts(r *big.Int, s *big.Int) *frost.Signature { + return mustFrostSignatureFromTECDSA(&tecdsa.Signature{R: r, S: s}) +} + +func mustFrostSignatureFromTECDSA(signature *tecdsa.Signature) *frost.Signature { + result, err := frostsigning.FromTECDSASignature(signature) + if err != nil { + panic(fmt.Sprintf("signature conversion failed: %v", err)) + } + + return result +} diff --git a/pkg/tbtc/signing.go b/pkg/tbtc/signing.go index 346b6b0446..0880323963 100644 --- a/pkg/tbtc/signing.go +++ b/pkg/tbtc/signing.go @@ -9,12 +9,12 @@ import ( "time" "github.com/keep-network/keep-core/pkg/clientinfo" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/announcer" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa" - "github.com/keep-network/keep-core/pkg/tecdsa/signing" "go.uber.org/zap" "golang.org/x/sync/semaphore" ) @@ -102,7 +102,7 @@ func (se *signingExecutor) signBatch( ctx context.Context, messages []*big.Int, startBlock uint64, -) ([]*tecdsa.Signature, error) { +) ([]*frost.Signature, error) { wallet := se.wallet() walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) @@ -139,7 +139,7 @@ func (se *signingExecutor) signBatch( ) signingStartBlock := startBlock // start block for the first signing - signatures := make([]*tecdsa.Signature, len(messages)) + signatures := make([]*frost.Signature, len(messages)) endBlocks := make([]uint64, len(messages)) for i, message := range messages { @@ -184,7 +184,7 @@ func (se *signingExecutor) sign( ctx context.Context, message *big.Int, startBlock uint64, -) (*tecdsa.Signature, *signingActivityReport, uint64, error) { +) (*frost.Signature, *signingActivityReport, uint64, error) { if lockAcquired := se.lock.TryAcquire(1); !lockAcquired { // Record failure metrics for lock acquisition failure if se.metricsRecorder != nil { @@ -223,7 +223,7 @@ func (se *signingExecutor) sign( ) type signingOutcome struct { - signature *tecdsa.Signature + signature *frost.Signature activityReport *signingActivityReport endBlock uint64 } diff --git a/pkg/tbtc/signing_done.go b/pkg/tbtc/signing_done.go index 58dfeccc83..1b49c51ee5 100644 --- a/pkg/tbtc/signing_done.go +++ b/pkg/tbtc/signing_done.go @@ -7,10 +7,10 @@ import ( "sync" "time" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa" - "github.com/keep-network/keep-core/pkg/tecdsa/signing" ) // signingDoneReceiveBuffer is a buffer for messages received from the broadcast @@ -35,7 +35,7 @@ type signingDoneMessage struct { senderID group.MemberIndex message *big.Int attemptNumber uint64 - signature *tecdsa.Signature + signature *frost.Signature endBlock uint64 } @@ -170,7 +170,7 @@ func (sdc *signingDoneCheck) waitUntilAllDone(ctx context.Context) ( case <-ticker.C: if sdc.expectedSignersCount == len(sdc.doneSigners) { - var signature *tecdsa.Signature + var signature *frost.Signature var latestEndBlock uint64 for _, doneMessage := range sdc.doneSigners { diff --git a/pkg/tbtc/signing_done_test.go b/pkg/tbtc/signing_done_test.go index 792edd6b68..720a6133df 100644 --- a/pkg/tbtc/signing_done_test.go +++ b/pkg/tbtc/signing_done_test.go @@ -14,12 +14,11 @@ import ( "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/chain/local_v1" + "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/net/local" "github.com/keep-network/keep-core/pkg/operator" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa" - "github.com/keep-network/keep-core/pkg/tecdsa/signing" ) // TestSigningDoneCheck is a happy path test. @@ -46,11 +45,7 @@ func TestSigningDoneCheck(t *testing.T) { attemptTimeoutBlock := uint64(1000) attemptMemberIndexes := memberIndexes[:groupParameters.HonestThreshold] result := &signing.Result{ - Signature: &tecdsa.Signature{ - R: big.NewInt(200), - S: big.NewInt(300), - RecoveryID: 2, - }, + Signature: mustFrostSignatureFromBigInts(big.NewInt(200), big.NewInt(300)), } type outcome struct { @@ -166,11 +161,7 @@ func TestSigningDoneCheck_MissingConfirmation(t *testing.T) { attemptTimeoutBlock := uint64(1000) attemptMemberIndexes := memberIndexes[:groupParameters.HonestThreshold] result := &signing.Result{ - Signature: &tecdsa.Signature{ - R: big.NewInt(200), - S: big.NewInt(300), - RecoveryID: 2, - }, + Signature: mustFrostSignatureFromBigInts(big.NewInt(200), big.NewInt(300)), } doneCheck.listen( @@ -229,18 +220,10 @@ func TestSigningDoneCheck_AnotherSignature(t *testing.T) { attemptTimeoutBlock := uint64(1000) attemptMemberIndexes := memberIndexes[:groupParameters.HonestThreshold] correctResult := &signing.Result{ - Signature: &tecdsa.Signature{ - R: big.NewInt(200), - S: big.NewInt(300), - RecoveryID: 2, - }, + Signature: mustFrostSignatureFromBigInts(big.NewInt(200), big.NewInt(300)), } incorrectResult := &signing.Result{ - Signature: &tecdsa.Signature{ - R: big.NewInt(201), - S: big.NewInt(300), - RecoveryID: 2, - }, + Signature: mustFrostSignatureFromBigInts(big.NewInt(201), big.NewInt(300)), } doneCheck.listen( diff --git a/pkg/tbtc/signing_loop.go b/pkg/tbtc/signing_loop.go index 7e787f1975..2cea6254ca 100644 --- a/pkg/tbtc/signing_loop.go +++ b/pkg/tbtc/signing_loop.go @@ -13,9 +13,9 @@ import ( "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa/retry" - "github.com/keep-network/keep-core/pkg/tecdsa/signing" "golang.org/x/exp/slices" ) diff --git a/pkg/tbtc/signing_loop_test.go b/pkg/tbtc/signing_loop_test.go index 93397a9ef2..f7bef8bd1e 100644 --- a/pkg/tbtc/signing_loop_test.go +++ b/pkg/tbtc/signing_loop_test.go @@ -11,9 +11,8 @@ import ( "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa" - "github.com/keep-network/keep-core/pkg/tecdsa/signing" ) func TestSigningRetryLoop(t *testing.T) { @@ -46,11 +45,7 @@ func TestSigningRetryLoop(t *testing.T) { } testResult := &signing.Result{ - Signature: &tecdsa.Signature{ - R: big.NewInt(300), - S: big.NewInt(400), - RecoveryID: 2, - }, + Signature: mustFrostSignatureFromBigInts(big.NewInt(300), big.NewInt(400)), } var tests = map[string]struct { diff --git a/pkg/tbtc/signing_test.go b/pkg/tbtc/signing_test.go index 9298ad7d7f..5505e63c72 100644 --- a/pkg/tbtc/signing_test.go +++ b/pkg/tbtc/signing_test.go @@ -38,8 +38,8 @@ func TestSigningExecutor_Sign(t *testing.T) { if !ecdsa.Verify( walletPublicKey, message.Bytes(), - signature.R, - signature.S, + new(big.Int).SetBytes(signature.R[:]), + new(big.Int).SetBytes(signature.S[:]), ) { t.Errorf("invalid signature: [%+v]", signature) } @@ -99,8 +99,8 @@ func TestSigningExecutor_SignBatch(t *testing.T) { if !ecdsa.Verify( walletPublicKey, messages[i].Bytes(), - signature.R, - signature.S, + new(big.Int).SetBytes(signature.R[:]), + new(big.Int).SetBytes(signature.S[:]), ) { t.Errorf("invalid signature [%v]: [%+v]", i, signature) } diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 1da076356b..321892ac6b 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -17,6 +17,7 @@ import ( "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/pkg/clientinfo" + "github.com/keep-network/keep-core/pkg/frost" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" "go.uber.org/zap" @@ -281,7 +282,7 @@ type walletSigningExecutor interface { ctx context.Context, messages []*big.Int, startBlock uint64, - ) ([]*tecdsa.Signature, error) + ) ([]*frost.Signature, error) } // walletTransactionExecutor is a component allowing to sign and broadcast @@ -354,8 +355,8 @@ func (wte *walletTransactionExecutor) signTransaction( containers := make([]*bitcoin.SignatureContainer, len(signatures)) for i, signature := range signatures { containers[i] = &bitcoin.SignatureContainer{ - R: signature.R, - S: signature.S, + R: new(big.Int).SetBytes(signature.R[:]), + S: new(big.Int).SetBytes(signature.S[:]), PublicKey: wte.executingWallet.publicKey, } } diff --git a/pkg/tbtc/wallet_test.go b/pkg/tbtc/wallet_test.go index 802e3aed3f..f4510f414c 100644 --- a/pkg/tbtc/wallet_test.go +++ b/pkg/tbtc/wallet_test.go @@ -8,8 +8,6 @@ import ( "encoding/binary" "encoding/hex" "fmt" - "github.com/keep-network/keep-core/pkg/chain" - "github.com/keep-network/keep-core/pkg/protocol/group" "math/big" "reflect" "sync" @@ -18,6 +16,9 @@ import ( "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" ) @@ -418,12 +419,12 @@ func generateWallet(privateKey *big.Int) wallet { type mockWalletSigningExecutor struct { signaturesMutex sync.Mutex - signatures map[[32]byte][]*tecdsa.Signature + signatures map[[32]byte][]*frost.Signature } func newMockWalletSigningExecutor() *mockWalletSigningExecutor { return &mockWalletSigningExecutor{ - signatures: make(map[[32]byte][]*tecdsa.Signature), + signatures: make(map[[32]byte][]*frost.Signature), } } @@ -431,7 +432,7 @@ func (mwse *mockWalletSigningExecutor) signBatch( ctx context.Context, messages []*big.Int, startBlock uint64, -) ([]*tecdsa.Signature, error) { +) ([]*frost.Signature, error) { mwse.signaturesMutex.Lock() defer mwse.signaturesMutex.Unlock() @@ -448,7 +449,7 @@ func (mwse *mockWalletSigningExecutor) signBatch( func (mwse *mockWalletSigningExecutor) setSignatures( messages []*big.Int, startBlock uint64, - signatures []*tecdsa.Signature, + signatures []*frost.Signature, ) { mwse.signaturesMutex.Lock() defer mwse.signaturesMutex.Unlock() From 2702c7a4ba0492deddccf535275a9ccd2864b2b9 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 09:30:14 -0600 Subject: [PATCH 04/96] Add FROST retry and coordinator attempt metadata path --- pkg/frost/retry/retry.go | 341 +++++++++++++++++++++++ pkg/frost/retry/retry_test.go | 251 +++++++++++++++++ pkg/frost/roast/coordinator.go | 41 +++ pkg/frost/roast/coordinator_test.go | 111 ++++++++ pkg/frost/signing/attempt.go | 34 +++ pkg/frost/signing/attempt_test.go | 41 +++ pkg/frost/signing/result.go | 2 + pkg/frost/signing/signing.go | 25 +- pkg/tbtc/signing.go | 54 +++- pkg/tbtc/signing_loop.go | 22 +- pkg/tbtc/signing_runtime_helpers_test.go | 34 +++ 11 files changed, 942 insertions(+), 14 deletions(-) create mode 100644 pkg/frost/retry/retry.go create mode 100644 pkg/frost/retry/retry_test.go create mode 100644 pkg/frost/roast/coordinator.go create mode 100644 pkg/frost/roast/coordinator_test.go create mode 100644 pkg/frost/signing/attempt.go create mode 100644 pkg/frost/signing/attempt_test.go create mode 100644 pkg/tbtc/signing_runtime_helpers_test.go diff --git a/pkg/frost/retry/retry.go b/pkg/frost/retry/retry.go new file mode 100644 index 0000000000..246e8b1fae --- /dev/null +++ b/pkg/frost/retry/retry.go @@ -0,0 +1,341 @@ +package retry + +import ( + "fmt" + "math/rand" + "sort" + + "github.com/keep-network/keep-core/pkg/chain" +) + +type byAddress []chain.Address + +func (ba byAddress) Len() int { return len(ba) } +func (ba byAddress) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] } +func (ba byAddress) Less(i, j int) bool { return ba[i] < ba[j] } + +func calculateSeatCount(groupMembers []chain.Address) map[chain.Address]uint { + operatorToSeatCount := make(map[chain.Address]uint) + for _, operator := range groupMembers { + operatorToSeatCount[operator]++ + } + return operatorToSeatCount +} + +// EvaluateRetryParticipantsForSigning takes in a slice of `groupMembers` and +// returns a subslice of those same members of length >= +// `retryParticipantsCount` randomly according to the provided `seed` and +// `retryCount`. +// +// This function is intended to be called during a signing protocol after a +// signing event has failed but *not* due to inactivity. Assuming that some of +// the `groupMembers` are sending corrupted information, either on purpose or +// accidentally, we keep trying to find a subset of `groupMembers` that is as +// small as possible, yet still larger than `retryParticipantsCount`. +// +// The `seed` param needs to vary on a per-message basis but must be the same +// seed between all operators for each invocation. This can be the hash of the +// message since cryptographically secure randomness isn't important. +// +// The `retryCount` denotes the number of the given retry, so that should be +// incremented after each attempt while the `seed` stays consistent on a +// per-message basis. +func EvaluateRetryParticipantsForSigning( + groupMembers []chain.Address, + seed int64, + retryCount uint, + retryParticipantsCount uint, +) ([]chain.Address, error) { + if int(retryParticipantsCount) > len(groupMembers) { + return nil, fmt.Errorf( + "asked for too many seats; [%d] seats were requested, but there are only [%d] available", + retryParticipantsCount, + len(groupMembers), + ) + } + operatorToSeatCount := calculateSeatCount(groupMembers) + + // #nosec G404 (insecure random number source (rand)) + // Shuffling operators for retries does not require secure randomness. + rng := rand.New(rand.NewSource(seed + int64(retryCount))) + + operators := make([]chain.Address, len(operatorToSeatCount)) + i := 0 + for operator := range operatorToSeatCount { + operators[i] = operator + i++ + } + sort.Sort(byAddress(operators)) + rng.Shuffle(len(operators), func(i, j int) { + operators[i], operators[j] = operators[j], operators[i] + }) + + seatCount := uint(0) + acceptedOperators := make(map[chain.Address]bool) + for j := 0; seatCount < retryParticipantsCount; j++ { + operator := operators[j] + seatCount += operatorToSeatCount[operator] + acceptedOperators[operator] = true + } + + var seats []chain.Address + for _, operator := range groupMembers { + if acceptedOperators[operator] { + seats = append(seats, operator) + } + } + return seats, nil +} + +// EvaluateRetryParticipantsForKeyGeneration takes in a slice of `groupMembers` +// and returns a subslice of those same members of length >= +// `retryParticipantsCount` randomly according to the provided `seed` and +// `retryCount`. +// +// This function is intended to be called during key generation after a failure +// *not* due to inactivity. Assuming that some of the `groupMembers` are +// sending corrupted information, either on purpose or accidentally, we keep +// trying to find a subset of `groupMembers` that is as large as possible by +// first excluding single operators, then pairs of operators, then triplets of +// operators. We use the `seed` param to generate randomness to shuffle the +// singles/pairs/triplets of operators to exclude and then use the `retryCount` +// param to select which single/pair/triplet to exclude. +// +// The `seed` param needs to vary on a per-message basis but must be the same +// seed between all operators for each invocation. This can be the hash of the +// message since cryptographically secure randomness isn't important. +// +// The `retryCount` denotes the number of the given retry, so that should be +// incremented after each attempt while the `seed` stays consistent on a +// per-message basis. +func EvaluateRetryParticipantsForKeyGeneration( + groupMembers []chain.Address, + seed int64, + retryCount uint, + retryParticipantsCount uint, +) ([]chain.Address, error) { + remainingTries := retryCount + if int(retryParticipantsCount) > len(groupMembers) { + return nil, fmt.Errorf( + "asked for too many seats; [%d] seats were requested, "+ + "but there are only [%d] available", + retryParticipantsCount, + len(groupMembers), + ) + } + operatorToSeatCount := calculateSeatCount(groupMembers) + // #nosec G404 (insecure random number source (rand)) + // Shuffling operators for retries does not require secure randomness. Unlike + // EvaluateRetryParticipantsForSigning above, we only want to use the seed as + // a source of randomness. The `retryCount` is used to select which operators + // to exclude after we shuffle them. + rng := rand.New(rand.NewSource(seed)) + + operators := make([]chain.Address, 0, len(operatorToSeatCount)) + for operator := range operatorToSeatCount { + // Only include the operators that have few enough seats such that if they + // were excluded we still have at least `retryParticipantsCount` seats. + if len(groupMembers)-int(operatorToSeatCount[operator]) >= int(retryParticipantsCount) { + operators = append(operators, operator) + } + } + sort.Sort(byAddress(operators)) + + usedOperators, tries, ok := excludeSingleOperator( + rng, + groupMembers, + int(remainingTries), + operatorToSeatCount, + operators, + ) + if ok { + return usedOperators, nil + } else { + remainingTries -= uint(tries) + } + + usedOperators, tries, ok = excludeOperatorPairs( + rng, + groupMembers, + int(remainingTries), + operatorToSeatCount, + operators, + int(retryParticipantsCount), + ) + if ok { + return usedOperators, nil + } else { + remainingTries -= uint(tries) + } + + usedOperators, tries, ok = excludeOperatorTriplets( + rng, + groupMembers, + int(remainingTries), + operatorToSeatCount, + operators, + int(retryParticipantsCount), + ) + if ok { + return usedOperators, nil + } else { + remainingTries -= uint(tries) + return nil, fmt.Errorf( + "the retry count [%d] was too large to handle; "+ + "tried every single, pair, and triplet, but still needed [%d] more retries", + retryCount, + remainingTries, + ) + } +} + +// excludeSingleOperator randomly excludes all of an operator's seats from a +// given `groupMembers`. It needs a pre-seeded random generator `rng`, and an +// `index`, which is expected to be inferred from a `retryCount`. +// +// It does this by shuffling a list of eligible-for-exclusion operators +// according to `rng`, selecting the operator according to `index`, and then +// filtering that operator out of `groupMembers`. +// +// In the case that `index` is larger than the number of eligible operators, it +// skips shuffling and returns the number of eligible operators, which is +// useful for determining the index of the operator pair to ignore. +func excludeSingleOperator( + rng *rand.Rand, + groupMembers []chain.Address, + index int, + operatorToSeatCount map[chain.Address]uint, + operators []chain.Address, +) ([]chain.Address, int, bool) { + if index < len(operators) { + rng.Shuffle(len(operators), func(i, j int) { + operators[i], operators[j] = operators[j], operators[i] + }) + removedOperator := operators[index] + usedOperators := make([]chain.Address, 0, len(groupMembers)) + for _, operator := range groupMembers { + if operator != removedOperator { + usedOperators = append(usedOperators, operator) + } + } + return usedOperators, 0, true + } else { + return nil, len(operators), false + } +} + +// excludeOperatorPairs randomly excludes all of a pair of operator's seats from a +// given `groupMembers`. It needs a pre-seeded random generator `rng`, and an +// `index`, which is expected to be inferred from a `retryCount`. +// +// It does this by shuffling a list of eligable-for-exclusion operators +// according to `rng`, selecting the operator according to `index`, and then +// filtering that operator pair out of `groupMembers`. +// +// In the case that `index` is larger than the number of eligible operator +// pairs, it skips shuffling and returns the number of eligible operators +// pairs, which is useful for determining the index of the operator triplet to +// ignore. +func excludeOperatorPairs( + rng *rand.Rand, + groupMembers []chain.Address, + index int, + operatorToSeatCount map[chain.Address]uint, + operators []chain.Address, + retryParticipantsCount int, +) ([]chain.Address, int, bool) { + pairIndexes := make([][2]int, 0, len(operators)*len(operators)) + for i := 0; i < len(operators)-1; i++ { + for j := i + 1; j < len(operators); j++ { + leftOperator := operators[i] + rightOperator := operators[j] + + // Only include the operators pairs that have few enough seats such that + // if they were excluded we still have at least `retryParticipantsCount` + // seats. + count := len(groupMembers) - + int(operatorToSeatCount[leftOperator]) - + int(operatorToSeatCount[rightOperator]) + if count >= int(retryParticipantsCount) { + pairIndexes = append(pairIndexes, [2]int{i, j}) + } + } + } + if index < len(pairIndexes) { + rng.Shuffle(len(pairIndexes), func(i, j int) { + pairIndexes[i], pairIndexes[j] = pairIndexes[j], pairIndexes[i] + }) + pair := pairIndexes[index] + leftOperator := operators[pair[0]] + rightOperator := operators[pair[1]] + usedOperators := make([]chain.Address, 0, len(groupMembers)) + for _, operator := range groupMembers { + if operator != leftOperator && operator != rightOperator { + usedOperators = append(usedOperators, operator) + } + } + return usedOperators, 0, true + } else { + return nil, len(pairIndexes), false + } +} + +// excludeOperatorTriplets randomly excludes all of a triplet of operator's seats from a +// given `groupMembers`. It needs a pre-seeded random generator `rng`, and an +// `index`, which is expected to be inferred from a `retryCount`. +// +// It does this by shuffling a list of eligable-for-exclusion operators +// according to `rng`, selecting the operator according to `index`, and then +// filtering that operator triplet out of `groupMembers`. +// +// In the case that `index` is larger than the number of eligible operator +// triplets, it skips shuffling and returns the number of eligible operators +// triplets, which is useful for logging errors. +func excludeOperatorTriplets( + rng *rand.Rand, + groupMembers []chain.Address, + index int, + operatorToSeatCount map[chain.Address]uint, + operators []chain.Address, + retryParticipantsCount int, +) ([]chain.Address, int, bool) { + tripletIndexes := make([][3]int, 0, len(operators)*len(operators)*len(operators)) + for i := 0; i < len(operators)-2; i++ { + for j := i + 1; j < len(operators)-1; j++ { + for k := j + 1; k < len(operators); k++ { + leftOperator := operators[i] + middleOperator := operators[j] + rightOperator := operators[j] + + // Only include the operators triples that have few enough seats such + // that if they were excluded we still have at least + // `retryParticipantsCount` seats. + count := len(groupMembers) - + int(operatorToSeatCount[leftOperator]) - + int(operatorToSeatCount[middleOperator]) - + int(operatorToSeatCount[rightOperator]) + if count >= int(retryParticipantsCount) { + tripletIndexes = append(tripletIndexes, [3]int{i, j, k}) + } + } + } + } + if index < len(tripletIndexes) { + rng.Shuffle(len(tripletIndexes), func(i, j int) { + tripletIndexes[i], tripletIndexes[j] = tripletIndexes[j], tripletIndexes[i] + }) + triplet := tripletIndexes[index] + leftOperator := operators[triplet[0]] + middleOperator := operators[triplet[1]] + rightOperator := operators[triplet[2]] + usedOperators := make([]chain.Address, 0, len(groupMembers)) + for _, operator := range groupMembers { + if operator != leftOperator && operator != middleOperator && operator != rightOperator { + usedOperators = append(usedOperators, operator) + } + } + return usedOperators, 0, true + } else { + return nil, len(tripletIndexes), false + } +} diff --git a/pkg/frost/retry/retry_test.go b/pkg/frost/retry/retry_test.go new file mode 100644 index 0000000000..24775c4c7e --- /dev/null +++ b/pkg/frost/retry/retry_test.go @@ -0,0 +1,251 @@ +package retry + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/keep-network/keep-core/pkg/chain" +) + +type groupMemberRandomizer func( + []chain.Address, + int64, + uint, + uint, +) ([]chain.Address, error) + +func TestEvaluateRetryParticipantsForSigning_100DifferentOperators(t *testing.T) { + groupMembers := make([]chain.Address, 100) + for i := 0; i < 100; i++ { + groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i)) + } + assertInvariants(t, EvaluateRetryParticipantsForSigning, groupMembers, int64(123), 0, 51) +} + +func TestEvaluateRetryParticipantsForSigning_FewOperators(t *testing.T) { + groupMembers := make([]chain.Address, 100) + for i := 0; i < 100; i++ { + groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i%3)) + } + assertInvariants(t, EvaluateRetryParticipantsForSigning, groupMembers, int64(456), 0, 51) +} + +func TestEvaluateRetryParticipantsForSigning_NotEnoughOperators(t *testing.T) { + groupMembers := make([]chain.Address, 50) + for i := 0; i < 50; i++ { + groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i)) + } + _, err := EvaluateRetryParticipantsForSigning(groupMembers, int64(123), 0, 51) + expectation := "asked for too many seats" + if err == nil { + t.Fatalf( + "unexpected error\nexpected: [%s]\nactual: [%v]", + fmt.Sprintf("%s...", expectation), + nil, + ) + } + if !strings.HasPrefix(err.Error(), expectation) { + t.Fatalf( + "unexpected error\nexpected: [%s]\nactual: [%s]", + fmt.Sprintf("%s...", expectation), + err.Error(), + ) + } +} + +func TestEvaluateRetryParticipantsForKeyGeneration_100DifferentOperators(t *testing.T) { + groupMembers := make([]chain.Address, 100) + for i := 0; i < 100; i++ { + groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i)) + } + assertInvariants(t, EvaluateRetryParticipantsForKeyGeneration, groupMembers, int64(123), 0, 90) +} + +func TestEvaluateRetryParticipantsForKeyGeneration_FewOperators(t *testing.T) { + groupMembers := make([]chain.Address, 100) + for i := 0; i < 100; i++ { + groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i%20)) + } + // There are 20 unique operators, and any 3 of them can be excluded while + // still being above the lower bound of 80 since each operator controls 5 + // seats. Thus, there are 20 single exclusions, 20 choose 2 = 190 pairs, and + // 20 choose 3 = 1140 triplets for a total of 20 + 190 + 1140 = 1350 total + // exclusions. + + // Single exclusion + assertInvariants(t, EvaluateRetryParticipantsForKeyGeneration, groupMembers, int64(456), 15, 80) + + // Pair Exclusion + assertInvariants(t, EvaluateRetryParticipantsForKeyGeneration, groupMembers, int64(456), 170, 80) + + // Triplet Exclusion + assertInvariants(t, EvaluateRetryParticipantsForKeyGeneration, groupMembers, int64(456), 1000, 80) + + // Too many! + _, err := EvaluateRetryParticipantsForKeyGeneration(groupMembers, int64(456), 1350, 80) + expectation := "the retry count [1350] was too large to handle; tried every single, pair, and triplet, but still needed [0] more retries" + if err.Error() != expectation { + t.Errorf( + "unexpected error\nexpected: [%s]\nactual: [%s]", + expectation, + err.Error(), + ) + } +} + +func TestEvaluateRetryParticipantsForKeyGeneration_NotEnoughOperators(t *testing.T) { + groupMembers := make([]chain.Address, 50) + for i := 0; i < 50; i++ { + groupMembers[i] = chain.Address(fmt.Sprintf("Operator-%d", i)) + } + _, err := EvaluateRetryParticipantsForKeyGeneration(groupMembers, int64(123), 0, 90) + expectation := "asked for too many seats" + if err == nil { + t.Fatalf( + "unexpected error\nexpected: [%s]\nactual: [%v]", + fmt.Sprintf("%s...", expectation), + nil, + ) + } + if !strings.HasPrefix(err.Error(), expectation) { + t.Fatalf( + "unexpected error\nexpected: [%s]\nactual: [%s]", + fmt.Sprintf("%s...", expectation), + err.Error(), + ) + } +} + +func isSubset( + t *testing.T, + groupMemberRandomizer groupMemberRandomizer, + groupMembers []chain.Address, + seed int64, + retryCount uint, + retryParticipantsCount uint, +) { + subset, err := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) + if err != nil { + t.Fatalf("unexpected error: [%s]", err) + } + memberMap := make(map[chain.Address]struct{}) + for _, operator := range groupMembers { + memberMap[operator] = struct{}{} + } + for _, operator := range subset { + if _, ok := memberMap[operator]; !ok { + t.Errorf("Subset member [%s] is not in the operator group.", operator) + } + } +} + +func isStable( + t *testing.T, + groupMemberRandomizer groupMemberRandomizer, + groupMembers []chain.Address, + seed int64, + retryCount uint, + retryParticipantsCount uint, +) { + subset, err := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) + if err != nil { + t.Fatalf("unexpected error: [%s]", err) + } + for i := 0; i < 30; i++ { + newSubset, err := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) + if err != nil { + t.Fatalf("unexpected error: [%s]", err) + } + if ok := reflect.DeepEqual(subset, newSubset); !ok { + t.Errorf( + "The subsets changed\nexpected: [%v]\nactual: [%v]", + subset, + newSubset, + ) + } + } +} + +func isLargeEnough( + t *testing.T, + groupMemberRandomizer groupMemberRandomizer, + groupMembers []chain.Address, + seed int64, + retryCount uint, + retryParticipantsCount uint, +) { + subset, err := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) + if err != nil { + t.Fatalf("unexpected error: [%s]", err) + } + if len(subset) < int(retryParticipantsCount) { + t.Errorf( + "Subset isn't large enough\nexpected: [%d+]\nactual: [%d]", + retryParticipantsCount, + len(subset), + ) + } +} + +// They don't all have to be different, but they shouldn't all be the same! +func affectedBySeed( + t *testing.T, + groupMemberRandomizer groupMemberRandomizer, + groupMembers []chain.Address, + originalSeed int64, + retryCount uint, + retryParticipantsCount uint, +) { + allTheSame := true + subset, err := groupMemberRandomizer(groupMembers, originalSeed, retryCount, retryParticipantsCount) + if err != nil { + t.Fatalf("unexpected error: [%s]", err) + } + for seed := int64(0); seed < 30 && allTheSame; seed++ { + newSubset, _ := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) + allTheSame = allTheSame && reflect.DeepEqual(subset, newSubset) + } + if allTheSame { + t.Error("The seed did not affect the subset generation. All subsets were the same.") + } +} + +// They don't all have to be different, but they shouldn't all be the same! +func affectedByRetryCount( + t *testing.T, + groupMemberRandomizer groupMemberRandomizer, + groupMembers []chain.Address, + seed int64, + originalRetryCount uint, + retryParticipantsCount uint, +) { + allTheSame := true + subset, err := groupMemberRandomizer(groupMembers, seed, originalRetryCount, retryParticipantsCount) + if err != nil { + t.Fatalf("unexpected error: [%s]", err) + } + for retryCount := uint(1); retryCount < 30 && allTheSame; retryCount++ { + newSubset, _ := groupMemberRandomizer(groupMembers, seed, retryCount, retryParticipantsCount) + allTheSame = allTheSame && reflect.DeepEqual(subset, newSubset) + } + if allTheSame { + t.Error("The seed did not affect the subset generation. All subsets were the same.") + } +} + +func assertInvariants( + t *testing.T, + groupMemberRandomizer groupMemberRandomizer, + groupMembers []chain.Address, + seed int64, + retryCount uint, + retryParticipantsCount uint, +) { + isSubset(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) + isStable(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) + isLargeEnough(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) + affectedBySeed(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) + affectedByRetryCount(t, groupMemberRandomizer, groupMembers, seed, retryCount, retryParticipantsCount) +} diff --git a/pkg/frost/roast/coordinator.go b/pkg/frost/roast/coordinator.go new file mode 100644 index 0000000000..5accd12607 --- /dev/null +++ b/pkg/frost/roast/coordinator.go @@ -0,0 +1,41 @@ +package roast + +import ( + "fmt" + "math/rand" + "sort" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// SelectCoordinator deterministically picks a coordinator from the included +// members set for a given attempt. +// +// Selection is pseudo-random but stable across all participants that use the +// same attempt seed and attempt number. +func SelectCoordinator( + includedMembersIndexes []group.MemberIndex, + attemptSeed int64, + attemptNumber uint, +) (group.MemberIndex, error) { + if len(includedMembersIndexes) == 0 { + return 0, fmt.Errorf("cannot select coordinator from empty member set") + } + + members := make([]group.MemberIndex, len(includedMembersIndexes)) + copy(members, includedMembersIndexes) + + // Sort first to make sure selection result is independent from input order. + sort.Slice(members, func(i, j int) bool { + return members[i] < members[j] + }) + + // #nosec G404 (insecure random number source (rand)) + // Coordinator shuffling needs deterministic, not cryptographic randomness. + rng := rand.New(rand.NewSource(attemptSeed + int64(attemptNumber))) + rng.Shuffle(len(members), func(i, j int) { + members[i], members[j] = members[j], members[i] + }) + + return members[0], nil +} diff --git a/pkg/frost/roast/coordinator_test.go b/pkg/frost/roast/coordinator_test.go new file mode 100644 index 0000000000..0847685de6 --- /dev/null +++ b/pkg/frost/roast/coordinator_test.go @@ -0,0 +1,111 @@ +package roast + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestSelectCoordinator_EmptySet(t *testing.T) { + _, err := SelectCoordinator([]group.MemberIndex{}, 100, 1) + if err == nil { + t.Fatal("expected coordinator selection error") + } +} + +func TestSelectCoordinator_Deterministic(t *testing.T) { + members := []group.MemberIndex{4, 1, 3, 2} + + first, err := SelectCoordinator(members, 12345, 2) + if err != nil { + t.Fatalf("selection failed: [%v]", err) + } + + for i := 0; i < 20; i++ { + again, err := SelectCoordinator(members, 12345, 2) + if err != nil { + t.Fatalf("selection failed on run [%d]: [%v]", i, err) + } + + if again != first { + t.Fatalf( + "non-deterministic coordinator\nexpected: [%v]\nactual: [%v]", + first, + again, + ) + } + } +} + +func TestSelectCoordinator_InputOrderIndependent(t *testing.T) { + left := []group.MemberIndex{1, 2, 3, 4, 5, 6} + right := []group.MemberIndex{6, 1, 5, 2, 4, 3} + + leftCoordinator, err := SelectCoordinator(left, 333, 4) + if err != nil { + t.Fatalf("left selection failed: [%v]", err) + } + + rightCoordinator, err := SelectCoordinator(right, 333, 4) + if err != nil { + t.Fatalf("right selection failed: [%v]", err) + } + + if leftCoordinator != rightCoordinator { + t.Fatalf( + "input order should not matter\nleft: [%v]\nright: [%v]", + leftCoordinator, + rightCoordinator, + ) + } +} + +func TestSelectCoordinator_AffectedByAttemptNumber(t *testing.T) { + members := []group.MemberIndex{1, 2, 3, 4, 5, 6} + first, err := SelectCoordinator(members, 777, 1) + if err != nil { + t.Fatalf("selection failed: [%v]", err) + } + + differentObserved := false + for attempt := uint(2); attempt <= 20; attempt++ { + candidate, err := SelectCoordinator(members, 777, attempt) + if err != nil { + t.Fatalf("selection failed for attempt [%d]: [%v]", attempt, err) + } + + if candidate != first { + differentObserved = true + break + } + } + + if !differentObserved { + t.Fatal("coordinator did not change for any attempt number") + } +} + +func TestSelectCoordinator_AffectedBySeed(t *testing.T) { + members := []group.MemberIndex{1, 2, 3, 4, 5, 6} + first, err := SelectCoordinator(members, 1000, 2) + if err != nil { + t.Fatalf("selection failed: [%v]", err) + } + + differentObserved := false + for seed := int64(1001); seed <= 1030; seed++ { + candidate, err := SelectCoordinator(members, seed, 2) + if err != nil { + t.Fatalf("selection failed for seed [%d]: [%v]", seed, err) + } + + if candidate != first { + differentObserved = true + break + } + } + + if !differentObserved { + t.Fatal("coordinator did not change for any seed") + } +} diff --git a/pkg/frost/signing/attempt.go b/pkg/frost/signing/attempt.go new file mode 100644 index 0000000000..c0071db6e5 --- /dev/null +++ b/pkg/frost/signing/attempt.go @@ -0,0 +1,34 @@ +package signing + +import "github.com/keep-network/keep-core/pkg/protocol/group" + +// Attempt describes runtime context for a signing attempt coordinated by ROAST. +type Attempt struct { + // Number is the 1-based signing attempt counter for the same message. + Number uint + // CoordinatorMemberIndex is the member coordinating this attempt. + CoordinatorMemberIndex group.MemberIndex + // IncludedMembersIndexes are members participating in this attempt. + IncludedMembersIndexes []group.MemberIndex + // ExcludedMembersIndexes are members excluded from this attempt. + ExcludedMembersIndexes []group.MemberIndex +} + +func cloneAttempt(attempt *Attempt) *Attempt { + if attempt == nil { + return nil + } + + return &Attempt{ + Number: attempt.Number, + CoordinatorMemberIndex: attempt.CoordinatorMemberIndex, + IncludedMembersIndexes: append( + []group.MemberIndex{}, + attempt.IncludedMembersIndexes..., + ), + ExcludedMembersIndexes: append( + []group.MemberIndex{}, + attempt.ExcludedMembersIndexes..., + ), + } +} diff --git a/pkg/frost/signing/attempt_test.go b/pkg/frost/signing/attempt_test.go new file mode 100644 index 0000000000..8d8f87fbc2 --- /dev/null +++ b/pkg/frost/signing/attempt_test.go @@ -0,0 +1,41 @@ +package signing + +import ( + "reflect" + "testing" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestCloneAttempt(t *testing.T) { + original := &Attempt{ + Number: 3, + CoordinatorMemberIndex: 7, + IncludedMembersIndexes: []group.MemberIndex{1, 2, 3, 7}, + ExcludedMembersIndexes: []group.MemberIndex{4, 5, 6, 8}, + } + + cloned := cloneAttempt(original) + if !reflect.DeepEqual(original, cloned) { + t.Fatalf("unexpected clone\nexpected: [%+v]\nactual: [%+v]", original, cloned) + } + + if &original.IncludedMembersIndexes[0] == &cloned.IncludedMembersIndexes[0] { + t.Fatal("included members slice should be copied") + } + + if &original.ExcludedMembersIndexes[0] == &cloned.ExcludedMembersIndexes[0] { + t.Fatal("excluded members slice should be copied") + } + + cloned.IncludedMembersIndexes[0] = 99 + if original.IncludedMembersIndexes[0] == cloned.IncludedMembersIndexes[0] { + t.Fatal("mutating clone should not mutate original") + } +} + +func TestCloneAttempt_Nil(t *testing.T) { + if cloneAttempt(nil) != nil { + t.Fatal("expected nil clone") + } +} diff --git a/pkg/frost/signing/result.go b/pkg/frost/signing/result.go index e02583057f..bff53d34b4 100644 --- a/pkg/frost/signing/result.go +++ b/pkg/frost/signing/result.go @@ -6,4 +6,6 @@ import "github.com/keep-network/keep-core/pkg/frost" type Result struct { // Signature is the BIP-340-style signature produced as result of signing. Signature *frost.Signature + // Attempt contains execution metadata for the attempt producing Signature. + Attempt *Attempt } diff --git a/pkg/frost/signing/signing.go b/pkg/frost/signing/signing.go index 40f03acf62..f6f511490d 100644 --- a/pkg/frost/signing/signing.go +++ b/pkg/frost/signing/signing.go @@ -31,7 +31,25 @@ func Execute( excludedMembersIndexes []group.MemberIndex, channel net.BroadcastChannel, membershipValidator *group.MembershipValidator, + attempt *Attempt, ) (*Result, error) { + if attempt != nil { + logger.Infof( + "[member:%v] executing FROST signing attempt [%v] "+ + "with coordinator [%v] (included: [%v], excluded: [%v])", + memberIndex, + attempt.Number, + attempt.CoordinatorMemberIndex, + attempt.IncludedMembersIndexes, + attempt.ExcludedMembersIndexes, + ) + } + + legacyExcludedMembersIndexes := excludedMembersIndexes + if attempt != nil && len(attempt.ExcludedMembersIndexes) > 0 { + legacyExcludedMembersIndexes = attempt.ExcludedMembersIndexes + } + legacyResult, err := legacySigning.Execute( ctx, logger, @@ -41,7 +59,7 @@ func Execute( privateKeyShare, groupSize, dishonestThreshold, - excludedMembersIndexes, + legacyExcludedMembersIndexes, channel, membershipValidator, ) @@ -54,7 +72,10 @@ func Execute( return nil, err } - return &Result{Signature: signature}, nil + return &Result{ + Signature: signature, + Attempt: cloneAttempt(attempt), + }, nil } // RegisterUnmarshallers initializes all required message unmarshallers. diff --git a/pkg/tbtc/signing.go b/pkg/tbtc/signing.go index 0880323963..4382127d05 100644 --- a/pkg/tbtc/signing.go +++ b/pkg/tbtc/signing.go @@ -10,6 +10,7 @@ import ( "github.com/keep-network/keep-core/pkg/clientinfo" "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/frost/roast" "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/generator" "github.com/keep-network/keep-core/pkg/net" @@ -291,12 +292,38 @@ func (se *signingExecutor) sign( zap.Uint64("attemptTimeoutBlock", attempt.timeoutBlock), ) + includedMembersIndexes := attemptIncludedMembersIndexes( + wallet.groupSize(), + attempt.excludedMembersIndexes, + ) + + coordinatorMemberIndex, err := roast.SelectCoordinator( + includedMembersIndexes, + signingAttemptSeed(message), + attempt.number, + ) + if err != nil { + return nil, 0, fmt.Errorf( + "cannot select signing coordinator for attempt [%v]: [%w]", + attempt.number, + err, + ) + } + + attemptInfo := &signing.Attempt{ + Number: attempt.number, + CoordinatorMemberIndex: coordinatorMemberIndex, + IncludedMembersIndexes: includedMembersIndexes, + ExcludedMembersIndexes: attempt.excludedMembersIndexes, + } + signingAttemptLogger.Infof( "[member:%v] starting signing protocol "+ - "with [%v] group members (excluded: [%v])", + "with [%v] group members (coordinator: [%v], excluded: [%v])", signer.signingGroupMemberIndex, - wallet.groupSize()-len(attempt.excludedMembersIndexes), - attempt.excludedMembersIndexes, + len(includedMembersIndexes), + coordinatorMemberIndex, + attemptInfo.ExcludedMembersIndexes, ) // Set up the attempt timeout signal. @@ -333,6 +360,7 @@ func (se *signingExecutor) sign( attempt.excludedMembersIndexes, se.broadcastChannel, se.membershipValidator, + attemptInfo, ) if err != nil { return nil, 0, err @@ -437,6 +465,26 @@ func (se *signingExecutor) wallet() wallet { return se.signers[0].wallet } +func attemptIncludedMembersIndexes( + groupSize int, + excludedMembersIndexes []group.MemberIndex, +) []group.MemberIndex { + excludedMembersIndexesSet := make(map[group.MemberIndex]bool) + for _, excludedMemberIndex := range excludedMembersIndexes { + excludedMembersIndexesSet[excludedMemberIndex] = true + } + + includedMembersIndexes := make([]group.MemberIndex, 0) + for i := 0; i < groupSize; i++ { + memberIndex := group.MemberIndex(i + 1) + if !excludedMembersIndexesSet[memberIndex] { + includedMembersIndexes = append(includedMembersIndexes, memberIndex) + } + } + + return includedMembersIndexes +} + // setMetricsRecorder sets the metrics recorder for the signing executor. func (se *signingExecutor) setMetricsRecorder(recorder interface { IncrementCounter(name string, value float64) diff --git a/pkg/tbtc/signing_loop.go b/pkg/tbtc/signing_loop.go index 2cea6254ca..bb4fd7dad8 100644 --- a/pkg/tbtc/signing_loop.go +++ b/pkg/tbtc/signing_loop.go @@ -13,9 +13,9 @@ import ( "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost/retry" "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/protocol/group" - "github.com/keep-network/keep-core/pkg/tecdsa/retry" "golang.org/x/exp/slices" ) @@ -45,6 +45,17 @@ func signingAttemptMaximumBlocks() uint { signingAttemptCoolDownBlocks } +// signingAttemptSeed computes a deterministic seed used for retry and +// coordinator selection for a given signed message. +func signingAttemptSeed(message *big.Int) int64 { + // Compute the 8-byte seed needed for the random retry algorithm. We take + // the first 8 bytes of the hash of the signed message. This allows us to + // not care in this piece of the code about the length of the message and + // how this message is proposed. + messageSha256 := sha256.Sum256(message.Bytes()) + return int64(binary.BigEndian.Uint64(messageSha256[:8])) +} + // signingAnnouncer represents a component responsible for exchanging readiness // announcements for the given signing attempt of the given message. type signingAnnouncer interface { @@ -108,13 +119,6 @@ func newSigningRetryLoop( announcer signingAnnouncer, doneCheck signingDoneCheckStrategy, ) *signingRetryLoop { - // Compute the 8-byte seed needed for the random retry algorithm. We take - // the first 8 bytes of the hash of the signed message. This allows us to - // not care in this piece of the code about the length of the message and - // how this message is proposed. - messageSha256 := sha256.Sum256(message.Bytes()) - attemptSeed := int64(binary.BigEndian.Uint64(messageSha256[:8])) - return &signingRetryLoop{ logger: logger, message: message, @@ -124,7 +128,7 @@ func newSigningRetryLoop( announcer: announcer, attemptCounter: 0, attemptStartBlock: initialStartBlock, - attemptSeed: attemptSeed, + attemptSeed: signingAttemptSeed(message), doneCheck: doneCheck, } } diff --git a/pkg/tbtc/signing_runtime_helpers_test.go b/pkg/tbtc/signing_runtime_helpers_test.go new file mode 100644 index 0000000000..42418e03d0 --- /dev/null +++ b/pkg/tbtc/signing_runtime_helpers_test.go @@ -0,0 +1,34 @@ +package tbtc + +import ( + "math/big" + "reflect" + "testing" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestAttemptIncludedMembersIndexes(t *testing.T) { + included := attemptIncludedMembersIndexes( + 6, + []group.MemberIndex{6, 2, 4, 2}, + ) + + expected := []group.MemberIndex{1, 3, 5} + if !reflect.DeepEqual(expected, included) { + t.Fatalf("unexpected included members\nexpected: [%v]\nactual: [%v]", expected, included) + } +} + +func TestSigningAttemptSeed(t *testing.T) { + first := signingAttemptSeed(big.NewInt(100)) + again := signingAttemptSeed(big.NewInt(100)) + if first != again { + t.Fatalf("seed should be stable\nfirst: [%v]\nagain: [%v]", first, again) + } + + second := signingAttemptSeed(big.NewInt(101)) + if first == second { + t.Fatal("different messages should produce different attempt seeds") + } +} From 3fc7c9faa9a5238334b748c79b02bd69c828ba21 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 10:40:04 -0600 Subject: [PATCH 05/96] Fix triplet retry eligibility to use third operator seat count --- pkg/frost/retry/retry.go | 2 +- pkg/frost/retry/retry_test.go | 39 ++++++++++++++++++++++++++++++++++ pkg/tecdsa/retry/retry.go | 2 +- pkg/tecdsa/retry/retry_test.go | 39 ++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/pkg/frost/retry/retry.go b/pkg/frost/retry/retry.go index 246e8b1fae..798d3bed30 100644 --- a/pkg/frost/retry/retry.go +++ b/pkg/frost/retry/retry.go @@ -305,7 +305,7 @@ func excludeOperatorTriplets( for k := j + 1; k < len(operators); k++ { leftOperator := operators[i] middleOperator := operators[j] - rightOperator := operators[j] + rightOperator := operators[k] // Only include the operators triples that have few enough seats such // that if they were excluded we still have at least diff --git a/pkg/frost/retry/retry_test.go b/pkg/frost/retry/retry_test.go index 24775c4c7e..5e0a16dbcd 100644 --- a/pkg/frost/retry/retry_test.go +++ b/pkg/frost/retry/retry_test.go @@ -2,6 +2,7 @@ package retry import ( "fmt" + "math/rand" "reflect" "strings" "testing" @@ -118,6 +119,44 @@ func TestEvaluateRetryParticipantsForKeyGeneration_NotEnoughOperators(t *testing } } +func TestExcludeOperatorTriplets_UsesThirdOperatorSeatCount(t *testing.T) { + groupMembers := []chain.Address{ + "A", "A", "A", + "B", + "C", "C", "C", + } + + operatorToSeatCount := calculateSeatCount(groupMembers) + operators := []chain.Address{"A", "B", "C"} + + // #nosec G404 (insecure random number source (rand)) + // Deterministic RNG is sufficient for deterministic unit tests. + rng := rand.New(rand.NewSource(1)) + + usedOperators, skippedTries, ok := excludeOperatorTriplets( + rng, + groupMembers, + 0, + operatorToSeatCount, + operators, + 2, + ) + + if ok { + t.Fatalf( + "expected no eligible triplet exclusions, got operators: [%v]", + usedOperators, + ) + } + + if skippedTries != 0 { + t.Fatalf( + "expected zero skipped tries when no triplet is eligible, got: [%d]", + skippedTries, + ) + } +} + func isSubset( t *testing.T, groupMemberRandomizer groupMemberRandomizer, diff --git a/pkg/tecdsa/retry/retry.go b/pkg/tecdsa/retry/retry.go index 246e8b1fae..798d3bed30 100644 --- a/pkg/tecdsa/retry/retry.go +++ b/pkg/tecdsa/retry/retry.go @@ -305,7 +305,7 @@ func excludeOperatorTriplets( for k := j + 1; k < len(operators); k++ { leftOperator := operators[i] middleOperator := operators[j] - rightOperator := operators[j] + rightOperator := operators[k] // Only include the operators triples that have few enough seats such // that if they were excluded we still have at least diff --git a/pkg/tecdsa/retry/retry_test.go b/pkg/tecdsa/retry/retry_test.go index 24775c4c7e..5e0a16dbcd 100644 --- a/pkg/tecdsa/retry/retry_test.go +++ b/pkg/tecdsa/retry/retry_test.go @@ -2,6 +2,7 @@ package retry import ( "fmt" + "math/rand" "reflect" "strings" "testing" @@ -118,6 +119,44 @@ func TestEvaluateRetryParticipantsForKeyGeneration_NotEnoughOperators(t *testing } } +func TestExcludeOperatorTriplets_UsesThirdOperatorSeatCount(t *testing.T) { + groupMembers := []chain.Address{ + "A", "A", "A", + "B", + "C", "C", "C", + } + + operatorToSeatCount := calculateSeatCount(groupMembers) + operators := []chain.Address{"A", "B", "C"} + + // #nosec G404 (insecure random number source (rand)) + // Deterministic RNG is sufficient for deterministic unit tests. + rng := rand.New(rand.NewSource(1)) + + usedOperators, skippedTries, ok := excludeOperatorTriplets( + rng, + groupMembers, + 0, + operatorToSeatCount, + operators, + 2, + ) + + if ok { + t.Fatalf( + "expected no eligible triplet exclusions, got operators: [%v]", + usedOperators, + ) + } + + if skippedTries != 0 { + t.Fatalf( + "expected zero skipped tries when no triplet is eligible, got: [%d]", + skippedTries, + ) + } +} + func isSubset( t *testing.T, groupMemberRandomizer groupMemberRandomizer, From b57775afbafc76d861972d7a207fd6d9ceecf75d Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 10:49:53 -0600 Subject: [PATCH 06/96] Add pluggable FROST signing backend execution seam --- pkg/frost/signing/backend.go | 61 +++++++++++ pkg/frost/signing/backend_test.go | 159 ++++++++++++++++++++++++++++ pkg/frost/signing/legacy_backend.go | 83 +++++++++++++++ pkg/frost/signing/request.go | 22 ++++ pkg/frost/signing/signing.go | 54 +++------- pkg/tbtc/signing.go | 1 - 6 files changed, 338 insertions(+), 42 deletions(-) create mode 100644 pkg/frost/signing/backend.go create mode 100644 pkg/frost/signing/backend_test.go create mode 100644 pkg/frost/signing/legacy_backend.go create mode 100644 pkg/frost/signing/request.go diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go new file mode 100644 index 0000000000..3a63bae178 --- /dev/null +++ b/pkg/frost/signing/backend.go @@ -0,0 +1,61 @@ +package signing + +import ( + "context" + "fmt" + "sync" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" +) + +// ExecutionBackend represents a pluggable backend used by the FROST signing +// runtime. This enables seamless replacement of the transitional legacy engine +// with a native FROST/FFI-backed implementation. +type ExecutionBackend interface { + Name() string + Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, + ) (*Result, error) + RegisterUnmarshallers(channel net.BroadcastChannel) +} + +var ( + executionBackendMutex sync.RWMutex + executionBackend ExecutionBackend = newLegacyExecutionBackend() +) + +func currentExecutionBackend() ExecutionBackend { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return executionBackend +} + +// SetExecutionBackend sets a runtime execution backend. +func SetExecutionBackend(backend ExecutionBackend) error { + if backend == nil { + return fmt.Errorf("execution backend is nil") + } + + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + executionBackend = backend + return nil +} + +// ResetExecutionBackend restores the default transitional legacy backend. +func ResetExecutionBackend() { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + executionBackend = newLegacyExecutionBackend() +} + +// CurrentExecutionBackendName returns the active backend name. +func CurrentExecutionBackendName() string { + return currentExecutionBackend().Name() +} diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go new file mode 100644 index 0000000000..d011e48c3b --- /dev/null +++ b/pkg/frost/signing/backend_test.go @@ -0,0 +1,159 @@ +package signing + +import ( + "context" + "math/big" + "reflect" + "testing" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +type mockExecutionBackend struct { + name string + + executeCalls int + lastRequest *Request + result *Result + err error + + registerUnmarshallersCalls int + lastChannel net.BroadcastChannel +} + +func (meb *mockExecutionBackend) Name() string { + return meb.name +} + +func (meb *mockExecutionBackend) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + meb.executeCalls++ + meb.lastRequest = request + return meb.result, meb.err +} + +func (meb *mockExecutionBackend) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + meb.registerUnmarshallersCalls++ + meb.lastChannel = channel +} + +func TestCurrentExecutionBackendName_Default(t *testing.T) { + ResetExecutionBackend() + if CurrentExecutionBackendName() != legacyExecutionBackendName { + t.Fatalf( + "unexpected default backend name\nexpected: [%s]\nactual: [%s]", + legacyExecutionBackendName, + CurrentExecutionBackendName(), + ) + } +} + +func TestSetExecutionBackend_Nil(t *testing.T) { + if err := SetExecutionBackend(nil); err == nil { + t.Fatal("expected nil backend error") + } +} + +func TestExecute_DelegatesToCurrentBackend(t *testing.T) { + ResetExecutionBackend() + t.Cleanup(ResetExecutionBackend) + + expectedResult := &Result{Signature: &frost.Signature{}} + backend := &mockExecutionBackend{ + name: "mock", + result: expectedResult, + } + + if err := SetExecutionBackend(backend); err != nil { + t.Fatalf("failed setting backend: [%v]", err) + } + + attempt := &Attempt{ + Number: 2, + CoordinatorMemberIndex: 5, + IncludedMembersIndexes: []group.MemberIndex{1, 2, 5}, + ExcludedMembersIndexes: []group.MemberIndex{3, 4, 6}, + } + + result, err := Execute( + context.Background(), + nil, + big.NewInt(100), + "session-id", + 1, + nil, + 10, + 4, + nil, + nil, + attempt, + ) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if result != expectedResult { + t.Fatalf( + "unexpected result\nexpected: [%+v]\nactual: [%+v]", + expectedResult, + result, + ) + } + + if backend.executeCalls != 1 { + t.Fatalf("unexpected execute calls count: [%d]", backend.executeCalls) + } + + received := backend.lastRequest + if received == nil { + t.Fatal("expected backend request") + } + + if received.Attempt == attempt { + t.Fatal("expected request attempt clone, got same pointer") + } + + if !reflect.DeepEqual(received.Attempt, attempt) { + t.Fatalf( + "unexpected request attempt\nexpected: [%+v]\nactual: [%+v]", + attempt, + received.Attempt, + ) + } + + received.Attempt.IncludedMembersIndexes[0] = 99 + if attempt.IncludedMembersIndexes[0] == 99 { + t.Fatal("mutating backend request attempt should not mutate caller attempt") + } +} + +func TestRegisterUnmarshallers_DelegatesToCurrentBackend(t *testing.T) { + ResetExecutionBackend() + t.Cleanup(ResetExecutionBackend) + + backend := &mockExecutionBackend{name: "mock"} + if err := SetExecutionBackend(backend); err != nil { + t.Fatalf("failed setting backend: [%v]", err) + } + + RegisterUnmarshallers(nil) + + if backend.registerUnmarshallersCalls != 1 { + t.Fatalf( + "unexpected register unmarshallers calls count: [%d]", + backend.registerUnmarshallersCalls, + ) + } + + if backend.lastChannel != nil { + t.Fatal("expected nil channel to be forwarded unchanged") + } +} diff --git a/pkg/frost/signing/legacy_backend.go b/pkg/frost/signing/legacy_backend.go new file mode 100644 index 0000000000..456fa05805 --- /dev/null +++ b/pkg/frost/signing/legacy_backend.go @@ -0,0 +1,83 @@ +package signing + +import ( + "context" + "fmt" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" + legacySigning "github.com/keep-network/keep-core/pkg/tecdsa/signing" +) + +const legacyExecutionBackendName = "legacy-tecdsa-bridge" + +type legacyExecutionBackend struct{} + +func newLegacyExecutionBackend() *legacyExecutionBackend { + return &legacyExecutionBackend{} +} + +func (leb *legacyExecutionBackend) Name() string { + return legacyExecutionBackendName +} + +func (leb *legacyExecutionBackend) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + if request == nil { + return nil, fmt.Errorf("request is nil") + } + + if request.Attempt != nil { + logger.Infof( + "[member:%v] executing FROST signing attempt [%v] "+ + "with coordinator [%v] (included: [%v], excluded: [%v])", + request.MemberIndex, + request.Attempt.Number, + request.Attempt.CoordinatorMemberIndex, + request.Attempt.IncludedMembersIndexes, + request.Attempt.ExcludedMembersIndexes, + ) + } + + excludedMembersIndexes := []group.MemberIndex{} + if request.Attempt != nil { + excludedMembersIndexes = request.Attempt.ExcludedMembersIndexes + } + + legacyResult, err := legacySigning.Execute( + ctx, + logger, + request.Message, + request.SessionID, + request.MemberIndex, + request.PrivateKeyShare, + request.GroupSize, + request.DishonestThreshold, + excludedMembersIndexes, + request.Channel, + request.MembershipValidator, + ) + if err != nil { + return nil, err + } + + signature, err := FromTECDSASignature(legacyResult.Signature) + if err != nil { + return nil, err + } + + return &Result{ + Signature: signature, + Attempt: cloneAttempt(request.Attempt), + }, nil +} + +func (leb *legacyExecutionBackend) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + legacySigning.RegisterUnmarshallers(channel) +} diff --git a/pkg/frost/signing/request.go b/pkg/frost/signing/request.go new file mode 100644 index 0000000000..fc94320f0b --- /dev/null +++ b/pkg/frost/signing/request.go @@ -0,0 +1,22 @@ +package signing + +import ( + "math/big" + + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +// Request carries execution input for a FROST signing backend. +type Request struct { + Message *big.Int + SessionID string + MemberIndex group.MemberIndex + PrivateKeyShare *tecdsa.PrivateKeyShare + GroupSize int + DishonestThreshold int + Channel net.BroadcastChannel + MembershipValidator *group.MembershipValidator + Attempt *Attempt +} diff --git a/pkg/frost/signing/signing.go b/pkg/frost/signing/signing.go index f6f511490d..593cfbb752 100644 --- a/pkg/frost/signing/signing.go +++ b/pkg/frost/signing/signing.go @@ -10,7 +10,6 @@ import ( "github.com/keep-network/keep-core/pkg/net" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" - legacySigning "github.com/keep-network/keep-core/pkg/tecdsa/signing" ) // Execute runs signing and returns a Schnorr-shaped 64-byte signature. @@ -28,61 +27,34 @@ func Execute( privateKeyShare *tecdsa.PrivateKeyShare, groupSize int, dishonestThreshold int, - excludedMembersIndexes []group.MemberIndex, channel net.BroadcastChannel, membershipValidator *group.MembershipValidator, attempt *Attempt, ) (*Result, error) { - if attempt != nil { - logger.Infof( - "[member:%v] executing FROST signing attempt [%v] "+ - "with coordinator [%v] (included: [%v], excluded: [%v])", - memberIndex, - attempt.Number, - attempt.CoordinatorMemberIndex, - attempt.IncludedMembersIndexes, - attempt.ExcludedMembersIndexes, - ) - } - - legacyExcludedMembersIndexes := excludedMembersIndexes - if attempt != nil && len(attempt.ExcludedMembersIndexes) > 0 { - legacyExcludedMembersIndexes = attempt.ExcludedMembersIndexes + request := &Request{ + Message: message, + SessionID: sessionID, + MemberIndex: memberIndex, + PrivateKeyShare: privateKeyShare, + GroupSize: groupSize, + DishonestThreshold: dishonestThreshold, + Channel: channel, + MembershipValidator: membershipValidator, + Attempt: cloneAttempt(attempt), } - legacyResult, err := legacySigning.Execute( + return currentExecutionBackend().Execute( ctx, logger, - message, - sessionID, - memberIndex, - privateKeyShare, - groupSize, - dishonestThreshold, - legacyExcludedMembersIndexes, - channel, - membershipValidator, + request, ) - if err != nil { - return nil, err - } - - signature, err := FromTECDSASignature(legacyResult.Signature) - if err != nil { - return nil, err - } - - return &Result{ - Signature: signature, - Attempt: cloneAttempt(attempt), - }, nil } // RegisterUnmarshallers initializes all required message unmarshallers. // For now, signing transport message formats are delegated to the legacy // engine implementation. func RegisterUnmarshallers(channel net.BroadcastChannel) { - legacySigning.RegisterUnmarshallers(channel) + currentExecutionBackend().RegisterUnmarshallers(channel) } // FromTECDSASignature maps a legacy signature to the fixed-width Schnorr diff --git a/pkg/tbtc/signing.go b/pkg/tbtc/signing.go index 4382127d05..7028de4a52 100644 --- a/pkg/tbtc/signing.go +++ b/pkg/tbtc/signing.go @@ -357,7 +357,6 @@ func (se *signingExecutor) sign( wallet.groupDishonestThreshold( se.groupParameters.HonestThreshold, ), - attempt.excludedMembersIndexes, se.broadcastChannel, se.membershipValidator, attemptInfo, From 952946fed001011d44eb181c64a76e59c375d31c Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 10:58:37 -0600 Subject: [PATCH 07/96] Wire tbtc config to selectable FROST signing backend --- pkg/frost/signing/backend.go | 28 +++++++++++++++++ pkg/frost/signing/backend_test.go | 45 +++++++++++++++++++++++++++ pkg/tbtc/node.go | 8 +++++ pkg/tbtc/node_signing_backend_test.go | 44 ++++++++++++++++++++++++++ pkg/tbtc/tbtc.go | 4 +++ 5 files changed, 129 insertions(+) create mode 100644 pkg/tbtc/node_signing_backend_test.go diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 3a63bae178..391470b69c 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -3,6 +3,7 @@ package signing import ( "context" "fmt" + "strings" "sync" "github.com/ipfs/go-log/v2" @@ -23,10 +24,20 @@ type ExecutionBackend interface { } var ( + // ErrNativeExecutionBackendUnavailable is returned when native backend is + // requested but not linked in the current build. + ErrNativeExecutionBackendUnavailable = fmt.Errorf( + "native FROST signing backend is unavailable in this build", + ) + executionBackendMutex sync.RWMutex executionBackend ExecutionBackend = newLegacyExecutionBackend() ) +// LegacyExecutionBackendName is a stable identifier of the transitional +// legacy tECDSA bridge backend. +const LegacyExecutionBackendName = legacyExecutionBackendName + func currentExecutionBackend() ExecutionBackend { executionBackendMutex.RLock() defer executionBackendMutex.RUnlock() @@ -59,3 +70,20 @@ func ResetExecutionBackend() { func CurrentExecutionBackendName() string { return currentExecutionBackend().Name() } + +// SetExecutionBackendByName configures the runtime backend by a stable name. +// +// Supported values: +// - "", "legacy", "legacy-tecdsa-bridge": transitional legacy bridge backend +// - "native", "ffi": reserved for native FROST backend (currently unavailable) +func SetExecutionBackendByName(name string) error { + switch strings.ToLower(strings.TrimSpace(name)) { + case "", "legacy", legacyExecutionBackendName: + ResetExecutionBackend() + return nil + case "native", "ffi": + return ErrNativeExecutionBackendUnavailable + default: + return fmt.Errorf("unknown FROST signing backend: [%s]", name) + } +} diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go index d011e48c3b..7190dbf58b 100644 --- a/pkg/frost/signing/backend_test.go +++ b/pkg/frost/signing/backend_test.go @@ -4,6 +4,7 @@ import ( "context" "math/big" "reflect" + "strings" "testing" "github.com/ipfs/go-log/v2" @@ -62,6 +63,50 @@ func TestSetExecutionBackend_Nil(t *testing.T) { } } +func TestSetExecutionBackendByName(t *testing.T) { + ResetExecutionBackend() + t.Cleanup(ResetExecutionBackend) + + if err := SetExecutionBackendByName(""); err != nil { + t.Fatalf("unexpected default backend config error: [%v]", err) + } + if CurrentExecutionBackendName() != legacyExecutionBackendName { + t.Fatalf( + "unexpected backend name for default config\\nexpected: [%s]\\nactual: [%s]", + legacyExecutionBackendName, + CurrentExecutionBackendName(), + ) + } + + if err := SetExecutionBackendByName("LEGACY"); err != nil { + t.Fatalf("unexpected legacy backend config error: [%v]", err) + } + if CurrentExecutionBackendName() != legacyExecutionBackendName { + t.Fatalf( + "unexpected backend name for legacy config\\nexpected: [%s]\\nactual: [%s]", + legacyExecutionBackendName, + CurrentExecutionBackendName(), + ) + } + + err := SetExecutionBackendByName("native") + if err == nil { + t.Fatal("expected native backend unavailable error") + } + if !strings.Contains(err.Error(), ErrNativeExecutionBackendUnavailable.Error()) { + t.Fatalf( + "unexpected native backend error\\nexpected substring: [%s]\\nactual: [%s]", + ErrNativeExecutionBackendUnavailable.Error(), + err.Error(), + ) + } + + err = SetExecutionBackendByName("unknown") + if err == nil { + t.Fatal("expected unknown backend error") + } +} + func TestExecute_DelegatesToCurrentBackend(t *testing.T) { ResetExecutionBackend() t.Cleanup(ResetExecutionBackend) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 39023ab165..913ffe185e 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -133,6 +133,10 @@ func newNode( proposalGenerator CoordinationProposalGenerator, config Config, ) (*node, error) { + if err := configureFrostSigningBackend(config); err != nil { + return nil, fmt.Errorf("cannot configure FROST signing backend: [%v]", err) + } + walletRegistry, err := newWalletRegistry( keyStorePersistance, chain.CalculateWalletID, @@ -193,6 +197,10 @@ func newNode( return node, nil } +func configureFrostSigningBackend(config Config) error { + return signing.SetExecutionBackendByName(config.FrostSigningBackend) +} + // setPerformanceMetrics sets the performance metrics recorder for the node // and wires it into components that support metrics. func (n *node) setPerformanceMetrics(metrics interface { diff --git a/pkg/tbtc/node_signing_backend_test.go b/pkg/tbtc/node_signing_backend_test.go new file mode 100644 index 0000000000..9695a8ec9f --- /dev/null +++ b/pkg/tbtc/node_signing_backend_test.go @@ -0,0 +1,44 @@ +package tbtc + +import ( + "errors" + "testing" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" +) + +func TestConfigureFrostSigningBackend_Default(t *testing.T) { + frostsigning.ResetExecutionBackend() + t.Cleanup(frostsigning.ResetExecutionBackend) + + err := configureFrostSigningBackend(Config{}) + if err != nil { + t.Fatalf("unexpected config error: [%v]", err) + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.LegacyExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.LegacyExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } +} + +func TestConfigureFrostSigningBackend_NativeUnavailable(t *testing.T) { + frostsigning.ResetExecutionBackend() + t.Cleanup(frostsigning.ResetExecutionBackend) + + err := configureFrostSigningBackend(Config{FrostSigningBackend: "native"}) + if err == nil { + t.Fatal("expected native backend config error") + } + + if !errors.Is(err, frostsigning.ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + frostsigning.ErrNativeExecutionBackendUnavailable, + err, + ) + } +} diff --git a/pkg/tbtc/tbtc.go b/pkg/tbtc/tbtc.go index 1cf700f164..1f1480eefe 100644 --- a/pkg/tbtc/tbtc.go +++ b/pkg/tbtc/tbtc.go @@ -65,6 +65,10 @@ type Config struct { PreParamsGenerationConcurrency int // Concurrency level for key-generation for tECDSA. KeyGenerationConcurrency int + // FrostSigningBackend selects the FROST signing backend implementation. + // Supported values are resolved by pkg/frost/signing.SetExecutionBackendByName. + // Empty value defaults to the transitional legacy bridge backend. + FrostSigningBackend string } // Initialize kicks off the TBTC by initializing internal state, ensuring From 0df807c688edcbac392221b2417b189377a94f41 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 11:12:13 -0600 Subject: [PATCH 08/96] Add native FROST signing backend scaffold --- pkg/frost/signing/backend.go | 63 ++++++++++++++- pkg/frost/signing/backend_test.go | 111 ++++++++++++++++++++++++-- pkg/frost/signing/native_backend.go | 60 ++++++++++++++ pkg/tbtc/node_signing_backend_test.go | 47 +++++++++++ 4 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 pkg/frost/signing/native_backend.go diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 391470b69c..8776a06327 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -30,14 +30,19 @@ var ( "native FROST signing backend is unavailable in this build", ) - executionBackendMutex sync.RWMutex - executionBackend ExecutionBackend = newLegacyExecutionBackend() + executionBackendMutex sync.RWMutex + executionBackend ExecutionBackend = newLegacyExecutionBackend() + nativeExecutionAdapter NativeExecutionAdapter ) // LegacyExecutionBackendName is a stable identifier of the transitional // legacy tECDSA bridge backend. const LegacyExecutionBackendName = legacyExecutionBackendName +// NativeExecutionBackendName is a stable identifier of the native FROST +// execution backend. +const NativeExecutionBackendName = nativeExecutionBackendName + func currentExecutionBackend() ExecutionBackend { executionBackendMutex.RLock() defer executionBackendMutex.RUnlock() @@ -82,8 +87,60 @@ func SetExecutionBackendByName(name string) error { ResetExecutionBackend() return nil case "native", "ffi": - return ErrNativeExecutionBackendUnavailable + nativeBackend, err := currentNativeExecutionBackend() + if err != nil { + return err + } + + return SetExecutionBackend(nativeBackend) default: return fmt.Errorf("unknown FROST signing backend: [%s]", name) } } + +// RegisterNativeExecutionAdapter sets a native adapter used by the +// native FROST execution backend. +func RegisterNativeExecutionAdapter(adapter NativeExecutionAdapter) error { + if adapter == nil { + return fmt.Errorf("native execution adapter is nil") + } + + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeExecutionAdapter = adapter + + return nil +} + +// UnregisterNativeExecutionAdapter clears the native adapter registration. +func UnregisterNativeExecutionAdapter() { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeExecutionAdapter = nil +} + +func currentNativeExecutionBackend() (ExecutionBackend, error) { + executionBackendMutex.RLock() + adapter := nativeExecutionAdapter + executionBackendMutex.RUnlock() + + if adapter == nil { + return nil, fmt.Errorf( + "%w: no native execution adapter registered", + ErrNativeExecutionBackendUnavailable, + ) + } + + backend, err := newNativeExecutionBackend(adapter) + if err != nil { + return nil, fmt.Errorf( + "%w: [%v]", + ErrNativeExecutionBackendUnavailable, + err, + ) + } + + return backend, nil +} diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go index 7190dbf58b..12cb075087 100644 --- a/pkg/frost/signing/backend_test.go +++ b/pkg/frost/signing/backend_test.go @@ -2,9 +2,9 @@ package signing import ( "context" + "errors" "math/big" "reflect" - "strings" "testing" "github.com/ipfs/go-log/v2" @@ -25,6 +25,16 @@ type mockExecutionBackend struct { lastChannel net.BroadcastChannel } +type mockNativeExecutionAdapter struct { + executeCalls int + lastRequest *Request + result *Result + err error + + registerUnmarshallersCalls int + lastChannel net.BroadcastChannel +} + func (meb *mockExecutionBackend) Name() string { return meb.name } @@ -46,6 +56,23 @@ func (meb *mockExecutionBackend) RegisterUnmarshallers( meb.lastChannel = channel } +func (mnea *mockNativeExecutionAdapter) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + mnea.executeCalls++ + mnea.lastRequest = request + return mnea.result, mnea.err +} + +func (mnea *mockNativeExecutionAdapter) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + mnea.registerUnmarshallersCalls++ + mnea.lastChannel = channel +} + func TestCurrentExecutionBackendName_Default(t *testing.T) { ResetExecutionBackend() if CurrentExecutionBackendName() != legacyExecutionBackendName { @@ -65,7 +92,9 @@ func TestSetExecutionBackend_Nil(t *testing.T) { func TestSetExecutionBackendByName(t *testing.T) { ResetExecutionBackend() + UnregisterNativeExecutionAdapter() t.Cleanup(ResetExecutionBackend) + t.Cleanup(UnregisterNativeExecutionAdapter) if err := SetExecutionBackendByName(""); err != nil { t.Fatalf("unexpected default backend config error: [%v]", err) @@ -93,11 +122,11 @@ func TestSetExecutionBackendByName(t *testing.T) { if err == nil { t.Fatal("expected native backend unavailable error") } - if !strings.Contains(err.Error(), ErrNativeExecutionBackendUnavailable.Error()) { + if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { t.Fatalf( - "unexpected native backend error\\nexpected substring: [%s]\\nactual: [%s]", - ErrNativeExecutionBackendUnavailable.Error(), - err.Error(), + "unexpected native backend error\\nexpected: [%v]\\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + err, ) } @@ -107,6 +136,78 @@ func TestSetExecutionBackendByName(t *testing.T) { } } +func TestSetExecutionBackendByName_NativeAdapterRegistered(t *testing.T) { + ResetExecutionBackend() + UnregisterNativeExecutionAdapter() + t.Cleanup(ResetExecutionBackend) + t.Cleanup(UnregisterNativeExecutionAdapter) + + expectedResult := &Result{Signature: &frost.Signature{}} + adapter := &mockNativeExecutionAdapter{ + result: expectedResult, + } + + if err := RegisterNativeExecutionAdapter(adapter); err != nil { + t.Fatalf("failed registering native execution adapter: [%v]", err) + } + + if err := SetExecutionBackendByName("ffi"); err != nil { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + + if CurrentExecutionBackendName() != nativeExecutionBackendName { + t.Fatalf( + "unexpected backend name for native config\\nexpected: [%s]\\nactual: [%s]", + nativeExecutionBackendName, + CurrentExecutionBackendName(), + ) + } + + executeResult, err := Execute( + context.Background(), + nil, + big.NewInt(100), + "session-id", + 1, + nil, + 10, + 4, + nil, + nil, + nil, + ) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if executeResult != expectedResult { + t.Fatalf( + "unexpected execute result\\nexpected: [%+v]\\nactual: [%+v]", + expectedResult, + executeResult, + ) + } + + if adapter.executeCalls != 1 { + t.Fatalf("unexpected native execute calls count: [%d]", adapter.executeCalls) + } + + RegisterUnmarshallers(nil) + + if adapter.registerUnmarshallersCalls != 1 { + t.Fatalf( + "unexpected native register unmarshallers calls count: [%d]", + adapter.registerUnmarshallersCalls, + ) + } +} + +func TestRegisterNativeExecutionAdapter_Nil(t *testing.T) { + if err := RegisterNativeExecutionAdapter(nil); err == nil { + t.Fatal("expected nil native adapter error") + } +} + func TestExecute_DelegatesToCurrentBackend(t *testing.T) { ResetExecutionBackend() t.Cleanup(ResetExecutionBackend) diff --git a/pkg/frost/signing/native_backend.go b/pkg/frost/signing/native_backend.go new file mode 100644 index 0000000000..a909ed9b21 --- /dev/null +++ b/pkg/frost/signing/native_backend.go @@ -0,0 +1,60 @@ +package signing + +import ( + "context" + "fmt" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" +) + +const nativeExecutionBackendName = "native-frost-ffi" + +// NativeExecutionAdapter is a transitional hook for wiring a future native +// FROST signing implementation (for example, cgo/FFI-backed). +type NativeExecutionAdapter interface { + Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, + ) (*Result, error) + RegisterUnmarshallers(channel net.BroadcastChannel) +} + +type nativeExecutionBackend struct { + adapter NativeExecutionAdapter +} + +func newNativeExecutionBackend( + adapter NativeExecutionAdapter, +) (*nativeExecutionBackend, error) { + if adapter == nil { + return nil, fmt.Errorf("native execution adapter is nil") + } + + return &nativeExecutionBackend{ + adapter: adapter, + }, nil +} + +func (neb *nativeExecutionBackend) Name() string { + return nativeExecutionBackendName +} + +func (neb *nativeExecutionBackend) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + if request == nil { + return nil, fmt.Errorf("request is nil") + } + + return neb.adapter.Execute(ctx, logger, request) +} + +func (neb *nativeExecutionBackend) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + neb.adapter.RegisterUnmarshallers(channel) +} diff --git a/pkg/tbtc/node_signing_backend_test.go b/pkg/tbtc/node_signing_backend_test.go index 9695a8ec9f..1f8d3bfd1e 100644 --- a/pkg/tbtc/node_signing_backend_test.go +++ b/pkg/tbtc/node_signing_backend_test.go @@ -1,15 +1,35 @@ package tbtc import ( + "context" "errors" "testing" + "github.com/ipfs/go-log/v2" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/net" ) +type noopNativeExecutionAdapter struct{} + +func (nnea *noopNativeExecutionAdapter) Execute( + ctx context.Context, + logger log.StandardLogger, + request *frostsigning.Request, +) (*frostsigning.Result, error) { + return nil, nil +} + +func (nnea *noopNativeExecutionAdapter) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { +} + func TestConfigureFrostSigningBackend_Default(t *testing.T) { frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) err := configureFrostSigningBackend(Config{}) if err != nil { @@ -27,7 +47,9 @@ func TestConfigureFrostSigningBackend_Default(t *testing.T) { func TestConfigureFrostSigningBackend_NativeUnavailable(t *testing.T) { frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) err := configureFrostSigningBackend(Config{FrostSigningBackend: "native"}) if err == nil { @@ -42,3 +64,28 @@ func TestConfigureFrostSigningBackend_NativeUnavailable(t *testing.T) { ) } } + +func TestConfigureFrostSigningBackend_NativeRegistered(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + err := frostsigning.RegisterNativeExecutionAdapter(&noopNativeExecutionAdapter{}) + if err != nil { + t.Fatalf("unexpected native adapter registration error: [%v]", err) + } + + err = configureFrostSigningBackend(Config{FrostSigningBackend: "native"}) + if err != nil { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } +} From 7817a136c621b329babd6ea527f2811ada2727c3 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 11:43:44 -0600 Subject: [PATCH 09/96] Expose tbtc FROST signing backend CLI flag --- cmd/flags.go | 7 +++++++ cmd/flags_test.go | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/cmd/flags.go b/cmd/flags.go index 6ce094c2e6..787eb52ade 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -308,6 +308,13 @@ func initTbtcFlags(cmd *cobra.Command, cfg *config.Config) { tbtc.DefaultKeyGenerationConcurrency, "tECDSA key generation concurrency.", ) + + cmd.Flags().StringVar( + &cfg.Tbtc.FrostSigningBackend, + "tbtc.frostSigningBackend", + "", + "FROST signing backend name (legacy, native, ffi). Empty value selects legacy.", + ) } // Initialize flags for Maintainer configuration. diff --git a/cmd/flags_test.go b/cmd/flags_test.go index 58ee1249ae..cee7fd2ed8 100644 --- a/cmd/flags_test.go +++ b/cmd/flags_test.go @@ -225,6 +225,13 @@ var cmdFlagsTests = map[string]struct { expectedValueFromFlag: 101, defaultValue: runtime.GOMAXPROCS(0), }, + "tbtc.frostSigningBackend": { + readValueFunc: func(c *config.Config) interface{} { return c.Tbtc.FrostSigningBackend }, + flagName: "--tbtc.frostSigningBackend", + flagValue: "native", + expectedValueFromFlag: "native", + defaultValue: "", + }, "maintainer.bitcoinDifficulty": { readValueFunc: func(c *config.Config) interface{} { return c.Maintainer.BitcoinDifficulty.Enabled }, flagName: "--bitcoinDifficulty", From fc4c7502e0f1b443036d48206d46d0adc1163395 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 12:01:23 -0600 Subject: [PATCH 10/96] Add build-tagged native FROST adapter bootstrap --- pkg/frost/signing/backend.go | 7 ++- .../native_adapter_build_default_test.go | 26 +++++++++ .../native_adapter_build_frost_native_test.go | 55 +++++++++++++++++++ .../signing/native_adapter_registration.go | 5 ++ .../native_adapter_registration_default.go | 5 ++ ...ative_adapter_registration_frost_native.go | 38 +++++++++++++ 6 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 pkg/frost/signing/native_adapter_build_default_test.go create mode 100644 pkg/frost/signing/native_adapter_build_frost_native_test.go create mode 100644 pkg/frost/signing/native_adapter_registration.go create mode 100644 pkg/frost/signing/native_adapter_registration_default.go create mode 100644 pkg/frost/signing/native_adapter_registration_frost_native.go diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 8776a06327..6fc3125807 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -29,6 +29,11 @@ var ( ErrNativeExecutionBackendUnavailable = fmt.Errorf( "native FROST signing backend is unavailable in this build", ) + // ErrNativeExecutionBackendNotImplemented is returned when native backend + // can be selected but does not provide a cryptographic execution engine yet. + ErrNativeExecutionBackendNotImplemented = fmt.Errorf( + "native FROST signing backend is not implemented", + ) executionBackendMutex sync.RWMutex executionBackend ExecutionBackend = newLegacyExecutionBackend() @@ -80,7 +85,7 @@ func CurrentExecutionBackendName() string { // // Supported values: // - "", "legacy", "legacy-tecdsa-bridge": transitional legacy bridge backend -// - "native", "ffi": reserved for native FROST backend (currently unavailable) +// - "native", "ffi": native FROST backend (requires registered native adapter) func SetExecutionBackendByName(name string) error { switch strings.ToLower(strings.TrimSpace(name)) { case "", "legacy", legacyExecutionBackendName: diff --git a/pkg/frost/signing/native_adapter_build_default_test.go b/pkg/frost/signing/native_adapter_build_default_test.go new file mode 100644 index 0000000000..c9c244292f --- /dev/null +++ b/pkg/frost/signing/native_adapter_build_default_test.go @@ -0,0 +1,26 @@ +//go:build !frost_native + +package signing + +import ( + "errors" + "testing" +) + +func TestNativeExecutionBackend_DefaultBuildUnavailable(t *testing.T) { + ResetExecutionBackend() + t.Cleanup(ResetExecutionBackend) + + err := SetExecutionBackendByName("native") + if err == nil { + t.Fatal("expected native backend unavailable error") + } + + if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected native backend error\nexpected: [%v]\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + err, + ) + } +} diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go new file mode 100644 index 0000000000..a571d86f5a --- /dev/null +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -0,0 +1,55 @@ +//go:build frost_native + +package signing + +import ( + "context" + "errors" + "testing" +) + +func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { + ResetExecutionBackend() + UnregisterNativeExecutionAdapter() + registerNativeExecutionAdapterForBuild() + t.Cleanup(ResetExecutionBackend) + t.Cleanup(UnregisterNativeExecutionAdapter) + + err := SetExecutionBackendByName("native") + if err != nil { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + + if CurrentExecutionBackendName() != NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + NativeExecutionBackendName, + CurrentExecutionBackendName(), + ) + } + + _, err = Execute( + context.Background(), + nil, + nil, + "session-id", + 1, + nil, + 10, + 4, + nil, + nil, + nil, + ) + if err == nil { + t.Fatal("expected placeholder native execution error") + } + + if !errors.Is(err, ErrNativeExecutionBackendNotImplemented) { + t.Fatalf( + "unexpected native execution error\nexpected: [%v]\nactual: [%v]", + ErrNativeExecutionBackendNotImplemented, + err, + ) + } +} diff --git a/pkg/frost/signing/native_adapter_registration.go b/pkg/frost/signing/native_adapter_registration.go new file mode 100644 index 0000000000..59c92a23ed --- /dev/null +++ b/pkg/frost/signing/native_adapter_registration.go @@ -0,0 +1,5 @@ +package signing + +func init() { + registerNativeExecutionAdapterForBuild() +} diff --git a/pkg/frost/signing/native_adapter_registration_default.go b/pkg/frost/signing/native_adapter_registration_default.go new file mode 100644 index 0000000000..065342b1cc --- /dev/null +++ b/pkg/frost/signing/native_adapter_registration_default.go @@ -0,0 +1,5 @@ +//go:build !frost_native + +package signing + +func registerNativeExecutionAdapterForBuild() {} diff --git a/pkg/frost/signing/native_adapter_registration_frost_native.go b/pkg/frost/signing/native_adapter_registration_frost_native.go new file mode 100644 index 0000000000..d04997c881 --- /dev/null +++ b/pkg/frost/signing/native_adapter_registration_frost_native.go @@ -0,0 +1,38 @@ +//go:build frost_native + +package signing + +import ( + "context" + "fmt" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" +) + +// buildTaggedNativeExecutionAdapter is a placeholder adapter wired when +// the frost_native build tag is enabled. +type buildTaggedNativeExecutionAdapter struct{} + +func registerNativeExecutionAdapterForBuild() { + err := RegisterNativeExecutionAdapter(&buildTaggedNativeExecutionAdapter{}) + if err != nil { + panic(fmt.Sprintf("failed to register build-tagged native adapter: [%v]", err)) + } +} + +func (btnea *buildTaggedNativeExecutionAdapter) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + return nil, fmt.Errorf( + "%w: build tag [frost_native] uses placeholder adapter", + ErrNativeExecutionBackendNotImplemented, + ) +} + +func (btnea *buildTaggedNativeExecutionAdapter) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { +} From f85b3d5be1bcb20ed58997c650a8babba31a7040 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 12:38:23 -0600 Subject: [PATCH 11/96] Test FROST backend selection in node startup --- pkg/tbtc/node.go | 2 +- pkg/tbtc/node_startup_signing_backend_test.go | 125 ++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 pkg/tbtc/node_startup_signing_backend_test.go diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 913ffe185e..df45f75c95 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -134,7 +134,7 @@ func newNode( config Config, ) (*node, error) { if err := configureFrostSigningBackend(config); err != nil { - return nil, fmt.Errorf("cannot configure FROST signing backend: [%v]", err) + return nil, fmt.Errorf("cannot configure FROST signing backend: %w", err) } walletRegistry, err := newWalletRegistry( diff --git a/pkg/tbtc/node_startup_signing_backend_test.go b/pkg/tbtc/node_startup_signing_backend_test.go new file mode 100644 index 0000000000..afb788eda9 --- /dev/null +++ b/pkg/tbtc/node_startup_signing_backend_test.go @@ -0,0 +1,125 @@ +package tbtc + +import ( + "errors" + "testing" + + "github.com/keep-network/keep-core/pkg/bitcoin" + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/generator" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/net/local" +) + +func TestNewNode_ConfiguresFrostSigningBackend_NativeUnavailable(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + groupParameters, localChain, netProvider, keyStorePersistence := + setupNewNodeSigningBackendTestDependencies(t) + + _, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + netProvider, + keyStorePersistence, + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{FrostSigningBackend: "native"}, + ) + if err == nil { + t.Fatal("expected newNode startup error for unavailable native backend") + } + + if !errors.Is(err, frostsigning.ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected newNode startup error\nexpected: [%v]\nactual: [%v]", + frostsigning.ErrNativeExecutionBackendUnavailable, + err, + ) + } +} + +func TestNewNode_ConfiguresFrostSigningBackend_NativeRegistered(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + err := frostsigning.RegisterNativeExecutionAdapter(&noopNativeExecutionAdapter{}) + if err != nil { + t.Fatalf("unexpected native adapter registration error: [%v]", err) + } + + groupParameters, localChain, netProvider, keyStorePersistence := + setupNewNodeSigningBackendTestDependencies(t) + + node, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + netProvider, + keyStorePersistence, + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{FrostSigningBackend: "native"}, + ) + if err != nil { + t.Fatalf("unexpected newNode startup error: [%v]", err) + } + + if node == nil { + t.Fatal("expected node instance") + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } +} + +func setupNewNodeSigningBackendTestDependencies( + t *testing.T, +) ( + *GroupParameters, + Chain, + net.Provider, + *mockPersistenceHandle, +) { + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + localChain := Connect() + netProvider := local.Connect() + signer := createMockSigner(t) + + walletPublicKeyHash := bitcoin.PublicKeyHash(signer.wallet.publicKey) + walletID, err := localChain.CalculateWalletID(signer.wallet.publicKey) + if err != nil { + t.Fatal(err) + } + + localChain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + EcdsaWalletID: walletID, + State: StateLive, + }, + ) + + return groupParameters, + localChain, + netProvider, + createMockKeyStorePersistence(t, signer) +} From 01ea9f44ad837de540d489316032b1fa393adbd0 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 12:50:16 -0600 Subject: [PATCH 12/96] Execute native-tag FROST adapter via legacy bridge --- pkg/frost/signing/backend.go | 14 +++-- .../native_adapter_build_frost_native_test.go | 28 +++------ .../signing/native_adapter_registration.go | 2 +- ...ative_adapter_registration_frost_native.go | 13 ++-- ...igning_native_backend_frost_native_test.go | 60 +++++++++++++++++++ 5 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 pkg/tbtc/signing_native_backend_frost_native_test.go diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 6fc3125807..705ca632c5 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -29,11 +29,6 @@ var ( ErrNativeExecutionBackendUnavailable = fmt.Errorf( "native FROST signing backend is unavailable in this build", ) - // ErrNativeExecutionBackendNotImplemented is returned when native backend - // can be selected but does not provide a cryptographic execution engine yet. - ErrNativeExecutionBackendNotImplemented = fmt.Errorf( - "native FROST signing backend is not implemented", - ) executionBackendMutex sync.RWMutex executionBackend ExecutionBackend = newLegacyExecutionBackend() @@ -126,6 +121,15 @@ func UnregisterNativeExecutionAdapter() { nativeExecutionAdapter = nil } +// RegisterNativeExecutionAdapterForBuild attempts to register the native +// adapter provided by the current build flavor. +// +// On default builds, this is a no-op. +// On `frost_native` builds, this registers the tagged native adapter. +func RegisterNativeExecutionAdapterForBuild() { + registerNativeExecutionAdapterForBuild() +} + func currentNativeExecutionBackend() (ExecutionBackend, error) { executionBackendMutex.RLock() adapter := nativeExecutionAdapter diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go index a571d86f5a..3a7a9c408d 100644 --- a/pkg/frost/signing/native_adapter_build_frost_native_test.go +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -4,14 +4,14 @@ package signing import ( "context" - "errors" + "strings" "testing" ) func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() - registerNativeExecutionAdapterForBuild() + RegisterNativeExecutionAdapterForBuild() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) @@ -28,27 +28,17 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { ) } - _, err = Execute( - context.Background(), - nil, - nil, - "session-id", - 1, - nil, - 10, - 4, - nil, - nil, - nil, - ) + adapter := &buildTaggedNativeExecutionAdapter{} + + _, err = adapter.Execute(context.Background(), nil, nil) if err == nil { - t.Fatal("expected placeholder native execution error") + t.Fatal("expected request validation error") } - if !errors.Is(err, ErrNativeExecutionBackendNotImplemented) { + if !strings.Contains(err.Error(), "request is nil") { t.Fatalf( - "unexpected native execution error\nexpected: [%v]\nactual: [%v]", - ErrNativeExecutionBackendNotImplemented, + "unexpected native execution error\nexpected substring: [%s]\nactual: [%v]", + "request is nil", err, ) } diff --git a/pkg/frost/signing/native_adapter_registration.go b/pkg/frost/signing/native_adapter_registration.go index 59c92a23ed..c4774da5a9 100644 --- a/pkg/frost/signing/native_adapter_registration.go +++ b/pkg/frost/signing/native_adapter_registration.go @@ -1,5 +1,5 @@ package signing func init() { - registerNativeExecutionAdapterForBuild() + RegisterNativeExecutionAdapterForBuild() } diff --git a/pkg/frost/signing/native_adapter_registration_frost_native.go b/pkg/frost/signing/native_adapter_registration_frost_native.go index d04997c881..7ed70a22db 100644 --- a/pkg/frost/signing/native_adapter_registration_frost_native.go +++ b/pkg/frost/signing/native_adapter_registration_frost_native.go @@ -10,8 +10,11 @@ import ( "github.com/keep-network/keep-core/pkg/net" ) -// buildTaggedNativeExecutionAdapter is a placeholder adapter wired when -// the frost_native build tag is enabled. +// buildTaggedNativeExecutionAdapter is a transitional adapter wired when the +// frost_native build tag is enabled. +// +// Until native FROST cryptographic execution is linked, this adapter delegates +// execution and unmarshaler wiring to the legacy tECDSA bridge runtime. type buildTaggedNativeExecutionAdapter struct{} func registerNativeExecutionAdapterForBuild() { @@ -26,13 +29,11 @@ func (btnea *buildTaggedNativeExecutionAdapter) Execute( logger log.StandardLogger, request *Request, ) (*Result, error) { - return nil, fmt.Errorf( - "%w: build tag [frost_native] uses placeholder adapter", - ErrNativeExecutionBackendNotImplemented, - ) + return newLegacyExecutionBackend().Execute(ctx, logger, request) } func (btnea *buildTaggedNativeExecutionAdapter) RegisterUnmarshallers( channel net.BroadcastChannel, ) { + newLegacyExecutionBackend().RegisterUnmarshallers(channel) } diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go new file mode 100644 index 0000000000..a14f1cac9c --- /dev/null +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -0,0 +1,60 @@ +//go:build frost_native + +package tbtc + +import ( + "context" + "crypto/ecdsa" + "math/big" + "testing" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" +) + +func TestSigningExecutor_Sign_NativeBackend(t *testing.T) { + executor := setupSigningExecutor(t) + + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.RegisterNativeExecutionAdapterForBuild() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + err := configureFrostSigningBackend(Config{FrostSigningBackend: "native"}) + if err != nil { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + message := big.NewInt(100) + startBlock := uint64(0) + + signature, _, endBlock, err := executor.sign(ctx, message, startBlock) + if err != nil { + t.Fatalf("unexpected native backend signing error: [%v]", err) + } + + walletPublicKey := executor.wallet().publicKey + if !ecdsa.Verify( + walletPublicKey, + message.Bytes(), + new(big.Int).SetBytes(signature.R[:]), + new(big.Int).SetBytes(signature.S[:]), + ) { + t.Fatalf("invalid signature: [%+v]", signature) + } + + if endBlock <= startBlock { + t.Fatal("wrong end block") + } +} From 9734656801fd87647fdd3e59346b8bddb0ecc10e Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 13:34:28 -0600 Subject: [PATCH 13/96] Harden backend-state test guidance after interim review --- pkg/frost/signing/backend.go | 3 +++ pkg/tbtc/signing_native_backend_frost_native_test.go | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 705ca632c5..58feaf068a 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -30,6 +30,9 @@ var ( "native FROST signing backend is unavailable in this build", ) + // executionBackend and nativeExecutionAdapter are process-global runtime + // state. Tests mutating this state must run sequentially; do not use + // t.Parallel in such tests. executionBackendMutex sync.RWMutex executionBackend ExecutionBackend = newLegacyExecutionBackend() nativeExecutionAdapter NativeExecutionAdapter diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index a14f1cac9c..7bc93c4db7 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -44,6 +44,10 @@ func TestSigningExecutor_Sign_NativeBackend(t *testing.T) { t.Fatalf("unexpected native backend signing error: [%v]", err) } + // Transitional path note: + // The current native-tag adapter delegates to legacy tECDSA signing. + // Switch this verification to Schnorr/BIP-340 once native FROST crypto + // execution is linked. walletPublicKey := executor.wallet().publicKey if !ecdsa.Verify( walletPublicKey, From f57aa099a8aae0dd54981140fd54df4254f4f882 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 13:47:54 -0600 Subject: [PATCH 14/96] Add native bridge scaffold with fallback routing --- .../native_adapter_build_frost_native_test.go | 254 +++++++++++++++++- ...ative_adapter_registration_frost_native.go | 55 +++- pkg/frost/signing/native_bridge.go | 58 ++++ 3 files changed, 360 insertions(+), 7 deletions(-) create mode 100644 pkg/frost/signing/native_bridge.go diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go index 3a7a9c408d..acb1cfc92d 100644 --- a/pkg/frost/signing/native_adapter_build_frost_native_test.go +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -4,10 +4,47 @@ package signing import ( "context" + "errors" "strings" "testing" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" ) +type mockNativeExecutionBridge struct { + available bool + + executeCalls int + lastRequest *Request + result *Result + err error + + registerUnmarshallersCalls int + lastChannel net.BroadcastChannel +} + +func (mneb *mockNativeExecutionBridge) IsAvailable() bool { + return mneb.available +} + +func (mneb *mockNativeExecutionBridge) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + mneb.executeCalls++ + mneb.lastRequest = request + return mneb.result, mneb.err +} + +func (mneb *mockNativeExecutionBridge) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + mneb.registerUnmarshallersCalls++ + mneb.lastChannel = channel +} + func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() @@ -28,7 +65,7 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { ) } - adapter := &buildTaggedNativeExecutionAdapter{} + adapter := newBuildTaggedNativeExecutionAdapter() _, err = adapter.Execute(context.Background(), nil, nil) if err == nil { @@ -43,3 +80,218 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { ) } } + +func TestBuildTaggedNativeExecutionAdapter_Execute_UsesNativeBridgeWhenAvailable( + t *testing.T, +) { + expectedResult := &Result{} + bridge := &mockNativeExecutionBridge{ + available: true, + result: expectedResult, + } + + fallback := &mockExecutionBackend{name: "fallback"} + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + result, err := adapter.Execute(context.Background(), nil, &Request{}) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if result != expectedResult { + t.Fatalf( + "unexpected result\nexpected: [%+v]\nactual: [%+v]", + expectedResult, + result, + ) + } + + if bridge.executeCalls != 1 { + t.Fatalf("unexpected bridge execute calls count: [%d]", bridge.executeCalls) + } + + if fallback.executeCalls != 0 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionAdapter_Execute_FallsBackWhenBridgeUnavailable( + t *testing.T, +) { + expectedResult := &Result{} + bridge := &mockNativeExecutionBridge{ + available: false, + } + + fallback := &mockExecutionBackend{ + name: "fallback", + result: expectedResult, + } + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + result, err := adapter.Execute(context.Background(), nil, &Request{}) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if result != expectedResult { + t.Fatalf( + "unexpected result\nexpected: [%+v]\nactual: [%+v]", + expectedResult, + result, + ) + } + + if bridge.executeCalls != 0 { + t.Fatalf("unexpected bridge execute calls count: [%d]", bridge.executeCalls) + } + + if fallback.executeCalls != 1 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionAdapter_Execute_FallsBackOnUnavailableBridgeError( + t *testing.T, +) { + expectedResult := &Result{} + bridge := &mockNativeExecutionBridge{ + available: true, + err: ErrNativeCryptographyUnavailable, + } + + fallback := &mockExecutionBackend{ + name: "fallback", + result: expectedResult, + } + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + result, err := adapter.Execute(context.Background(), nil, &Request{}) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if result != expectedResult { + t.Fatalf( + "unexpected result\nexpected: [%+v]\nactual: [%+v]", + expectedResult, + result, + ) + } + + if bridge.executeCalls != 1 { + t.Fatalf("unexpected bridge execute calls count: [%d]", bridge.executeCalls) + } + + if fallback.executeCalls != 1 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionAdapter_Execute_ReturnsBridgeError( + t *testing.T, +) { + bridgeError := errors.New("bridge failure") + bridge := &mockNativeExecutionBridge{ + available: true, + err: bridgeError, + } + + fallback := &mockExecutionBackend{name: "fallback"} + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + _, err := adapter.Execute(context.Background(), nil, &Request{}) + if err == nil { + t.Fatal("expected execute error") + } + + if !errors.Is(err, bridgeError) { + t.Fatalf( + "unexpected execute error\nexpected: [%v]\nactual: [%v]", + bridgeError, + err, + ) + } + + if fallback.executeCalls != 0 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_UsesNativeWhenAvailable( + t *testing.T, +) { + bridge := &mockNativeExecutionBridge{ + available: true, + } + + fallback := &mockExecutionBackend{name: "fallback"} + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + adapter.RegisterUnmarshallers(nil) + + if bridge.registerUnmarshallersCalls != 1 { + t.Fatalf( + "unexpected bridge register unmarshallers calls count: [%d]", + bridge.registerUnmarshallersCalls, + ) + } + + if fallback.registerUnmarshallersCalls != 0 { + t.Fatalf( + "unexpected fallback register unmarshallers calls count: [%d]", + fallback.registerUnmarshallersCalls, + ) + } +} + +func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_FallsBackWhenUnavailable( + t *testing.T, +) { + bridge := &mockNativeExecutionBridge{ + available: false, + } + + fallback := &mockExecutionBackend{name: "fallback"} + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + adapter.RegisterUnmarshallers(nil) + + if bridge.registerUnmarshallersCalls != 0 { + t.Fatalf( + "unexpected bridge register unmarshallers calls count: [%d]", + bridge.registerUnmarshallersCalls, + ) + } + + if fallback.registerUnmarshallersCalls != 1 { + t.Fatalf( + "unexpected fallback register unmarshallers calls count: [%d]", + fallback.registerUnmarshallersCalls, + ) + } +} diff --git a/pkg/frost/signing/native_adapter_registration_frost_native.go b/pkg/frost/signing/native_adapter_registration_frost_native.go index 7ed70a22db..2557291421 100644 --- a/pkg/frost/signing/native_adapter_registration_frost_native.go +++ b/pkg/frost/signing/native_adapter_registration_frost_native.go @@ -4,6 +4,7 @@ package signing import ( "context" + "errors" "fmt" "github.com/ipfs/go-log/v2" @@ -13,27 +14,69 @@ import ( // buildTaggedNativeExecutionAdapter is a transitional adapter wired when the // frost_native build tag is enabled. // -// Until native FROST cryptographic execution is linked, this adapter delegates -// execution and unmarshaler wiring to the legacy tECDSA bridge runtime. -type buildTaggedNativeExecutionAdapter struct{} +// The adapter uses a native execution bridge when available and falls back to +// the legacy tECDSA bridge runtime only when native cryptography is +// unavailable. +type buildTaggedNativeExecutionAdapter struct { + nativeBridge nativeExecutionBridge + fallback ExecutionBackend +} func registerNativeExecutionAdapterForBuild() { - err := RegisterNativeExecutionAdapter(&buildTaggedNativeExecutionAdapter{}) + err := RegisterNativeExecutionAdapter(newBuildTaggedNativeExecutionAdapter()) if err != nil { panic(fmt.Sprintf("failed to register build-tagged native adapter: [%v]", err)) } } +func newBuildTaggedNativeExecutionAdapter() *buildTaggedNativeExecutionAdapter { + return &buildTaggedNativeExecutionAdapter{ + nativeBridge: newNativeExecutionBridge(), + fallback: newLegacyExecutionBackend(), + } +} + func (btnea *buildTaggedNativeExecutionAdapter) Execute( ctx context.Context, logger log.StandardLogger, request *Request, ) (*Result, error) { - return newLegacyExecutionBackend().Execute(ctx, logger, request) + if btnea.nativeBridge != nil && btnea.nativeBridge.IsAvailable() { + result, err := btnea.nativeBridge.Execute(ctx, logger, request) + if err == nil { + return result, nil + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + return nil, fmt.Errorf("native bridge execution failed: [%w]", err) + } + + if logger != nil { + logger.Warnf( + "native FROST cryptography unavailable; falling back to legacy bridge backend: [%v]", + err, + ) + } + } + + if btnea.fallback == nil { + return nil, fmt.Errorf("fallback execution backend is nil") + } + + return btnea.fallback.Execute(ctx, logger, request) } func (btnea *buildTaggedNativeExecutionAdapter) RegisterUnmarshallers( channel net.BroadcastChannel, ) { - newLegacyExecutionBackend().RegisterUnmarshallers(channel) + if btnea.nativeBridge != nil && btnea.nativeBridge.IsAvailable() { + btnea.nativeBridge.RegisterUnmarshallers(channel) + return + } + + if btnea.fallback == nil { + return + } + + btnea.fallback.RegisterUnmarshallers(channel) } diff --git a/pkg/frost/signing/native_bridge.go b/pkg/frost/signing/native_bridge.go new file mode 100644 index 0000000000..df65d89fc0 --- /dev/null +++ b/pkg/frost/signing/native_bridge.go @@ -0,0 +1,58 @@ +package signing + +import ( + "context" + "errors" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" +) + +var ( + // ErrNativeCryptographyUnavailable indicates that native FROST + // cryptographic execution is not linked in the current build. + // + // The frost_native adapter handles this condition by falling back to the + // legacy bridge backend. + ErrNativeCryptographyUnavailable = errors.New( + "native FROST cryptographic execution is unavailable", + ) +) + +// nativeExecutionBridge defines a native cryptographic execution entrypoint +// used by the frost_native adapter. +// +// The current implementation returns ErrNativeCryptographyUnavailable. Future +// FFI-backed integrations should provide an available bridge implementation. +type nativeExecutionBridge interface { + IsAvailable() bool + Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, + ) (*Result, error) + RegisterUnmarshallers(channel net.BroadcastChannel) +} + +func newNativeExecutionBridge() nativeExecutionBridge { + return &unlinkedNativeExecutionBridge{} +} + +type unlinkedNativeExecutionBridge struct{} + +func (uneb *unlinkedNativeExecutionBridge) IsAvailable() bool { + return false +} + +func (uneb *unlinkedNativeExecutionBridge) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + return nil, ErrNativeCryptographyUnavailable +} + +func (uneb *unlinkedNativeExecutionBridge) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { +} From 7efb2f99d4a800f1f875aa8e63e6e1e4f4060650 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 14:56:55 -0600 Subject: [PATCH 15/96] Split native and ffi backend fallback semantics --- cmd/flags.go | 4 +- pkg/frost/signing/backend.go | 42 ++++++- pkg/frost/signing/backend_test.go | 28 +++++ .../native_adapter_build_frost_native_test.go | 110 ++++++++++++++++++ ...ative_adapter_registration_frost_native.go | 20 +++- pkg/tbtc/node_signing_backend_test.go | 45 +++++++ pkg/tbtc/node_startup_signing_backend_test.go | 75 ++++++++++++ pkg/tbtc/tbtc.go | 3 + 8 files changed, 321 insertions(+), 6 deletions(-) diff --git a/cmd/flags.go b/cmd/flags.go index 787eb52ade..097a673466 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -313,7 +313,9 @@ func initTbtcFlags(cmd *cobra.Command, cfg *config.Config) { &cfg.Tbtc.FrostSigningBackend, "tbtc.frostSigningBackend", "", - "FROST signing backend name (legacy, native, ffi). Empty value selects legacy.", + "FROST signing backend name (legacy, native, ffi). "+ + "`native` allows transitional legacy fallback; `ffi` requires native execution. "+ + "Empty value selects legacy.", ) } diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 58feaf068a..4fac3e20bf 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -36,6 +36,7 @@ var ( executionBackendMutex sync.RWMutex executionBackend ExecutionBackend = newLegacyExecutionBackend() nativeExecutionAdapter NativeExecutionAdapter + nativeExecutionMode = nativeExecutionModeFallbackAllowed ) // LegacyExecutionBackendName is a stable identifier of the transitional @@ -46,6 +47,17 @@ const LegacyExecutionBackendName = legacyExecutionBackendName // execution backend. const NativeExecutionBackendName = nativeExecutionBackendName +type nativeExecutionModeValue uint8 + +const ( + // nativeExecutionModeFallbackAllowed means the native adapter may fall back + // to transitional legacy execution when native cryptography is unavailable. + nativeExecutionModeFallbackAllowed nativeExecutionModeValue = iota + // nativeExecutionModeStrict requires native cryptographic execution and + // does not allow fallback to transitional legacy execution. + nativeExecutionModeStrict +) + func currentExecutionBackend() ExecutionBackend { executionBackendMutex.RLock() defer executionBackendMutex.RUnlock() @@ -72,6 +84,7 @@ func ResetExecutionBackend() { defer executionBackendMutex.Unlock() executionBackend = newLegacyExecutionBackend() + nativeExecutionMode = nativeExecutionModeFallbackAllowed } // CurrentExecutionBackendName returns the active backend name. @@ -83,13 +96,24 @@ func CurrentExecutionBackendName() string { // // Supported values: // - "", "legacy", "legacy-tecdsa-bridge": transitional legacy bridge backend -// - "native", "ffi": native FROST backend (requires registered native adapter) +// - "native": native route with transitional fallback to legacy when native +// cryptography is unavailable +// - "ffi": strict native route; no fallback to legacy execution func SetExecutionBackendByName(name string) error { switch strings.ToLower(strings.TrimSpace(name)) { case "", "legacy", legacyExecutionBackendName: ResetExecutionBackend() return nil - case "native", "ffi": + case "native": + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + nativeBackend, err := currentNativeExecutionBackend() + if err != nil { + return err + } + + return SetExecutionBackend(nativeBackend) + case "ffi": + setNativeExecutionMode(nativeExecutionModeStrict) nativeBackend, err := currentNativeExecutionBackend() if err != nil { return err @@ -101,6 +125,20 @@ func SetExecutionBackendByName(name string) error { } } +func setNativeExecutionMode(mode nativeExecutionModeValue) { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeExecutionMode = mode +} + +func nativeExecutionFallbackAllowed() bool { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return nativeExecutionMode == nativeExecutionModeFallbackAllowed +} + // RegisterNativeExecutionAdapter sets a native adapter used by the // native FROST execution backend. func RegisterNativeExecutionAdapter(adapter NativeExecutionAdapter) error { diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go index 12cb075087..ddc5898ca9 100644 --- a/pkg/frost/signing/backend_test.go +++ b/pkg/frost/signing/backend_test.go @@ -129,6 +129,24 @@ func TestSetExecutionBackendByName(t *testing.T) { err, ) } + if !nativeExecutionFallbackAllowed() { + t.Fatal("expected fallback-allowed mode for native backend selection") + } + + err = SetExecutionBackendByName("ffi") + if err == nil { + t.Fatal("expected ffi backend unavailable error") + } + if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected ffi backend error\\nexpected: [%v]\\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + err, + ) + } + if nativeExecutionFallbackAllowed() { + t.Fatal("expected strict mode for ffi backend selection") + } err = SetExecutionBackendByName("unknown") if err == nil { @@ -162,6 +180,16 @@ func TestSetExecutionBackendByName_NativeAdapterRegistered(t *testing.T) { CurrentExecutionBackendName(), ) } + if nativeExecutionFallbackAllowed() { + t.Fatal("expected strict mode for ffi backend selection") + } + + if err := SetExecutionBackendByName("native"); err != nil { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + if !nativeExecutionFallbackAllowed() { + t.Fatal("expected fallback-allowed mode for native backend selection") + } executeResult, err := Execute( context.Background(), diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go index acb1cfc92d..870104e4f1 100644 --- a/pkg/frost/signing/native_adapter_build_frost_native_test.go +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -234,6 +234,87 @@ func TestBuildTaggedNativeExecutionAdapter_Execute_ReturnsBridgeError( } } +func TestBuildTaggedNativeExecutionAdapter_Execute_StrictModeNoFallbackWhenUnavailable( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeStrict) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + bridge := &mockNativeExecutionBridge{ + available: false, + } + + fallback := &mockExecutionBackend{ + name: "fallback", + result: &Result{}, + } + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + _, err := adapter.Execute(context.Background(), nil, &Request{}) + if err == nil { + t.Fatal("expected execute error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected execute error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if fallback.executeCalls != 0 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionAdapter_Execute_StrictModeNoFallbackOnUnavailableError( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeStrict) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + bridge := &mockNativeExecutionBridge{ + available: true, + err: ErrNativeCryptographyUnavailable, + } + + fallback := &mockExecutionBackend{ + name: "fallback", + result: &Result{}, + } + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + _, err := adapter.Execute(context.Background(), nil, &Request{}) + if err == nil { + t.Fatal("expected execute error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected execute error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if fallback.executeCalls != 0 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_UsesNativeWhenAvailable( t *testing.T, ) { @@ -295,3 +376,32 @@ func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_FallsBackWhenUn ) } } + +func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_StrictModeNoFallback( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeStrict) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + bridge := &mockNativeExecutionBridge{ + available: false, + } + + fallback := &mockExecutionBackend{name: "fallback"} + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridge: bridge, + fallback: fallback, + } + + adapter.RegisterUnmarshallers(nil) + + if fallback.registerUnmarshallersCalls != 0 { + t.Fatalf( + "unexpected fallback register unmarshallers calls count: [%d]", + fallback.registerUnmarshallersCalls, + ) + } +} diff --git a/pkg/frost/signing/native_adapter_registration_frost_native.go b/pkg/frost/signing/native_adapter_registration_frost_native.go index 2557291421..1ef6534853 100644 --- a/pkg/frost/signing/native_adapter_registration_frost_native.go +++ b/pkg/frost/signing/native_adapter_registration_frost_native.go @@ -14,9 +14,11 @@ import ( // buildTaggedNativeExecutionAdapter is a transitional adapter wired when the // frost_native build tag is enabled. // -// The adapter uses a native execution bridge when available and falls back to -// the legacy tECDSA bridge runtime only when native cryptography is -// unavailable. +// The adapter uses a native execution bridge when available. +// +// Backend mode behavior: +// - `native`: fallback to legacy bridge when native cryptography is unavailable +// - `ffi`: no fallback; native cryptographic execution is required type buildTaggedNativeExecutionAdapter struct { nativeBridge nativeExecutionBridge fallback ExecutionBackend @@ -51,6 +53,10 @@ func (btnea *buildTaggedNativeExecutionAdapter) Execute( return nil, fmt.Errorf("native bridge execution failed: [%w]", err) } + if !nativeExecutionFallbackAllowed() { + return nil, err + } + if logger != nil { logger.Warnf( "native FROST cryptography unavailable; falling back to legacy bridge backend: [%v]", @@ -59,6 +65,10 @@ func (btnea *buildTaggedNativeExecutionAdapter) Execute( } } + if !nativeExecutionFallbackAllowed() { + return nil, ErrNativeCryptographyUnavailable + } + if btnea.fallback == nil { return nil, fmt.Errorf("fallback execution backend is nil") } @@ -74,6 +84,10 @@ func (btnea *buildTaggedNativeExecutionAdapter) RegisterUnmarshallers( return } + if !nativeExecutionFallbackAllowed() { + return + } + if btnea.fallback == nil { return } diff --git a/pkg/tbtc/node_signing_backend_test.go b/pkg/tbtc/node_signing_backend_test.go index 1f8d3bfd1e..b652dad140 100644 --- a/pkg/tbtc/node_signing_backend_test.go +++ b/pkg/tbtc/node_signing_backend_test.go @@ -65,6 +65,26 @@ func TestConfigureFrostSigningBackend_NativeUnavailable(t *testing.T) { } } +func TestConfigureFrostSigningBackend_FFIUnavailable(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + if err == nil { + t.Fatal("expected ffi backend config error") + } + + if !errors.Is(err, frostsigning.ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + frostsigning.ErrNativeExecutionBackendUnavailable, + err, + ) + } +} + func TestConfigureFrostSigningBackend_NativeRegistered(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() @@ -89,3 +109,28 @@ func TestConfigureFrostSigningBackend_NativeRegistered(t *testing.T) { ) } } + +func TestConfigureFrostSigningBackend_FFIRegistered(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + err := frostsigning.RegisterNativeExecutionAdapter(&noopNativeExecutionAdapter{}) + if err != nil { + t.Fatalf("unexpected native adapter registration error: [%v]", err) + } + + err = configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + if err != nil { + t.Fatalf("unexpected ffi backend config error: [%v]", err) + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } +} diff --git a/pkg/tbtc/node_startup_signing_backend_test.go b/pkg/tbtc/node_startup_signing_backend_test.go index afb788eda9..4162814113 100644 --- a/pkg/tbtc/node_startup_signing_backend_test.go +++ b/pkg/tbtc/node_startup_signing_backend_test.go @@ -44,6 +44,39 @@ func TestNewNode_ConfiguresFrostSigningBackend_NativeUnavailable(t *testing.T) { } } +func TestNewNode_ConfiguresFrostSigningBackend_FFIUnavailable(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + groupParameters, localChain, netProvider, keyStorePersistence := + setupNewNodeSigningBackendTestDependencies(t) + + _, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + netProvider, + keyStorePersistence, + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{FrostSigningBackend: "ffi"}, + ) + if err == nil { + t.Fatal("expected newNode startup error for unavailable ffi backend") + } + + if !errors.Is(err, frostsigning.ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected newNode startup error\nexpected: [%v]\nactual: [%v]", + frostsigning.ErrNativeExecutionBackendUnavailable, + err, + ) + } +} + func TestNewNode_ConfiguresFrostSigningBackend_NativeRegistered(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() @@ -86,6 +119,48 @@ func TestNewNode_ConfiguresFrostSigningBackend_NativeRegistered(t *testing.T) { } } +func TestNewNode_ConfiguresFrostSigningBackend_FFIRegistered(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + err := frostsigning.RegisterNativeExecutionAdapter(&noopNativeExecutionAdapter{}) + if err != nil { + t.Fatalf("unexpected native adapter registration error: [%v]", err) + } + + groupParameters, localChain, netProvider, keyStorePersistence := + setupNewNodeSigningBackendTestDependencies(t) + + node, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + netProvider, + keyStorePersistence, + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{FrostSigningBackend: "ffi"}, + ) + if err != nil { + t.Fatalf("unexpected newNode startup error: [%v]", err) + } + + if node == nil { + t.Fatal("expected node instance") + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } +} + func setupNewNodeSigningBackendTestDependencies( t *testing.T, ) ( diff --git a/pkg/tbtc/tbtc.go b/pkg/tbtc/tbtc.go index 1f1480eefe..64736b5ece 100644 --- a/pkg/tbtc/tbtc.go +++ b/pkg/tbtc/tbtc.go @@ -68,6 +68,9 @@ type Config struct { // FrostSigningBackend selects the FROST signing backend implementation. // Supported values are resolved by pkg/frost/signing.SetExecutionBackendByName. // Empty value defaults to the transitional legacy bridge backend. + // `native` allows transitional legacy fallback when native cryptographic + // execution is unavailable. `ffi` requires native execution and does not + // allow fallback. FrostSigningBackend string } From e42bf4313cb7408d2a107bb44d836012398b5034 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 15:36:12 -0600 Subject: [PATCH 16/96] Fail fast for strict ffi mode availability --- pkg/frost/signing/backend.go | 17 ++++++ pkg/frost/signing/backend_test.go | 55 +++++++++++++++++++ .../native_adapter_build_frost_native_test.go | 21 +++++++ ...ative_adapter_registration_frost_native.go | 4 ++ ...igning_native_backend_frost_native_test.go | 30 ++++++++++ 5 files changed, 127 insertions(+) diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 4fac3e20bf..8c6fdf83da 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -23,6 +23,10 @@ type ExecutionBackend interface { RegisterUnmarshallers(channel net.BroadcastChannel) } +type nativeExecutionAvailabilityReporter interface { + NativeExecutionAvailable() bool +} + var ( // ErrNativeExecutionBackendUnavailable is returned when native backend is // requested but not linked in the current build. @@ -174,6 +178,7 @@ func RegisterNativeExecutionAdapterForBuild() { func currentNativeExecutionBackend() (ExecutionBackend, error) { executionBackendMutex.RLock() adapter := nativeExecutionAdapter + mode := nativeExecutionMode executionBackendMutex.RUnlock() if adapter == nil { @@ -183,6 +188,18 @@ func currentNativeExecutionBackend() (ExecutionBackend, error) { ) } + if mode == nativeExecutionModeStrict { + if reporter, ok := adapter.(nativeExecutionAvailabilityReporter); ok { + if !reporter.NativeExecutionAvailable() { + return nil, fmt.Errorf( + "%w: %w", + ErrNativeExecutionBackendUnavailable, + ErrNativeCryptographyUnavailable, + ) + } + } + } + backend, err := newNativeExecutionBackend(adapter) if err != nil { return nil, fmt.Errorf( diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go index ddc5898ca9..18feff234a 100644 --- a/pkg/frost/signing/backend_test.go +++ b/pkg/frost/signing/backend_test.go @@ -35,6 +35,11 @@ type mockNativeExecutionAdapter struct { lastChannel net.BroadcastChannel } +type mockNativeExecutionAdapterWithAvailability struct { + *mockNativeExecutionAdapter + nativeExecutionAvailable bool +} + func (meb *mockExecutionBackend) Name() string { return meb.name } @@ -73,6 +78,10 @@ func (mnea *mockNativeExecutionAdapter) RegisterUnmarshallers( mnea.lastChannel = channel } +func (mneawa *mockNativeExecutionAdapterWithAvailability) NativeExecutionAvailable() bool { + return mneawa.nativeExecutionAvailable +} + func TestCurrentExecutionBackendName_Default(t *testing.T) { ResetExecutionBackend() if CurrentExecutionBackendName() != legacyExecutionBackendName { @@ -230,6 +239,52 @@ func TestSetExecutionBackendByName_NativeAdapterRegistered(t *testing.T) { } } +func TestSetExecutionBackendByName_FFIStrictAvailabilityCheck(t *testing.T) { + ResetExecutionBackend() + UnregisterNativeExecutionAdapter() + t.Cleanup(ResetExecutionBackend) + t.Cleanup(UnregisterNativeExecutionAdapter) + + adapter := &mockNativeExecutionAdapterWithAvailability{ + mockNativeExecutionAdapter: &mockNativeExecutionAdapter{}, + nativeExecutionAvailable: false, + } + + if err := RegisterNativeExecutionAdapter(adapter); err != nil { + t.Fatalf("failed registering native execution adapter: [%v]", err) + } + + err := SetExecutionBackendByName("ffi") + if err == nil { + t.Fatal("expected ffi backend unavailable error") + } + if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected ffi backend error\\nexpected: [%v]\\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + err, + ) + } + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected strict-mode availability error\\nexpected: [%v]\\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if err := SetExecutionBackendByName("native"); err != nil { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + if CurrentExecutionBackendName() != nativeExecutionBackendName { + t.Fatalf( + "unexpected backend name for native config\\nexpected: [%s]\\nactual: [%s]", + nativeExecutionBackendName, + CurrentExecutionBackendName(), + ) + } +} + func TestRegisterNativeExecutionAdapter_Nil(t *testing.T) { if err := RegisterNativeExecutionAdapter(nil); err == nil { t.Fatal("expected nil native adapter error") diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go index 870104e4f1..65e15fe0bc 100644 --- a/pkg/frost/signing/native_adapter_build_frost_native_test.go +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -79,6 +79,27 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { err, ) } + + err = SetExecutionBackendByName("ffi") + if err == nil { + t.Fatal("expected strict ffi backend unavailable error") + } + + if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected ffi backend error\nexpected: [%v]\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + err, + ) + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected ffi native-availability error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } } func TestBuildTaggedNativeExecutionAdapter_Execute_UsesNativeBridgeWhenAvailable( diff --git a/pkg/frost/signing/native_adapter_registration_frost_native.go b/pkg/frost/signing/native_adapter_registration_frost_native.go index 1ef6534853..56a00a8882 100644 --- a/pkg/frost/signing/native_adapter_registration_frost_native.go +++ b/pkg/frost/signing/native_adapter_registration_frost_native.go @@ -38,6 +38,10 @@ func newBuildTaggedNativeExecutionAdapter() *buildTaggedNativeExecutionAdapter { } } +func (btnea *buildTaggedNativeExecutionAdapter) NativeExecutionAvailable() bool { + return btnea.nativeBridge != nil && btnea.nativeBridge.IsAvailable() +} + func (btnea *buildTaggedNativeExecutionAdapter) Execute( ctx context.Context, logger log.StandardLogger, diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index 7bc93c4db7..8fffd9a5f5 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -5,12 +5,42 @@ package tbtc import ( "context" "crypto/ecdsa" + "errors" "math/big" "testing" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" ) +func TestConfigureFrostSigningBackend_FFIStrictUnavailable_BuildAdapter(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.RegisterNativeExecutionAdapterForBuild() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + if err == nil { + t.Fatal("expected strict ffi backend configuration error") + } + + if !errors.Is(err, frostsigning.ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected strict ffi backend error\nexpected: [%v]\nactual: [%v]", + frostsigning.ErrNativeExecutionBackendUnavailable, + err, + ) + } + + if !errors.Is(err, frostsigning.ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected strict ffi native-availability error\nexpected: [%v]\nactual: [%v]", + frostsigning.ErrNativeCryptographyUnavailable, + err, + ) + } +} + func TestSigningExecutor_Sign_NativeBackend(t *testing.T) { executor := setupSigningExecutor(t) From 606e73dfc650ea5ad2150a0039aa9014a7436774 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 15:48:15 -0600 Subject: [PATCH 17/96] Add dynamic native bridge registration for ffi --- pkg/frost/signing/backend.go | 15 ++-- pkg/frost/signing/backend_test.go | 12 ++++ .../native_adapter_build_frost_native_test.go | 69 ++++++++++++++----- ...ative_adapter_registration_frost_native.go | 29 +++++--- pkg/frost/signing/native_bridge.go | 42 ++++++++++- 5 files changed, 130 insertions(+), 37 deletions(-) diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 8c6fdf83da..aca70a4b87 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -34,13 +34,14 @@ var ( "native FROST signing backend is unavailable in this build", ) - // executionBackend and nativeExecutionAdapter are process-global runtime - // state. Tests mutating this state must run sequentially; do not use - // t.Parallel in such tests. - executionBackendMutex sync.RWMutex - executionBackend ExecutionBackend = newLegacyExecutionBackend() - nativeExecutionAdapter NativeExecutionAdapter - nativeExecutionMode = nativeExecutionModeFallbackAllowed + // executionBackend, nativeExecutionAdapter, and registeredNativeExecBridge + // are process-global runtime state. Tests mutating this state must run + // sequentially; do not use t.Parallel in such tests. + executionBackendMutex sync.RWMutex + executionBackend ExecutionBackend = newLegacyExecutionBackend() + nativeExecutionAdapter NativeExecutionAdapter + registeredNativeExecBridge NativeExecutionBridge + nativeExecutionMode = nativeExecutionModeFallbackAllowed ) // LegacyExecutionBackendName is a stable identifier of the transitional diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go index 18feff234a..067737a594 100644 --- a/pkg/frost/signing/backend_test.go +++ b/pkg/frost/signing/backend_test.go @@ -102,8 +102,10 @@ func TestSetExecutionBackend_Nil(t *testing.T) { func TestSetExecutionBackendByName(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() + UnregisterNativeExecutionBridge() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) + t.Cleanup(UnregisterNativeExecutionBridge) if err := SetExecutionBackendByName(""); err != nil { t.Fatalf("unexpected default backend config error: [%v]", err) @@ -166,8 +168,10 @@ func TestSetExecutionBackendByName(t *testing.T) { func TestSetExecutionBackendByName_NativeAdapterRegistered(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() + UnregisterNativeExecutionBridge() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) + t.Cleanup(UnregisterNativeExecutionBridge) expectedResult := &Result{Signature: &frost.Signature{}} adapter := &mockNativeExecutionAdapter{ @@ -242,8 +246,10 @@ func TestSetExecutionBackendByName_NativeAdapterRegistered(t *testing.T) { func TestSetExecutionBackendByName_FFIStrictAvailabilityCheck(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() + UnregisterNativeExecutionBridge() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) + t.Cleanup(UnregisterNativeExecutionBridge) adapter := &mockNativeExecutionAdapterWithAvailability{ mockNativeExecutionAdapter: &mockNativeExecutionAdapter{}, @@ -291,6 +297,12 @@ func TestRegisterNativeExecutionAdapter_Nil(t *testing.T) { } } +func TestRegisterNativeExecutionBridge_Nil(t *testing.T) { + if err := RegisterNativeExecutionBridge(nil); err == nil { + t.Fatal("expected nil native bridge error") + } +} + func TestExecute_DelegatesToCurrentBackend(t *testing.T) { ResetExecutionBackend() t.Cleanup(ResetExecutionBackend) diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go index 65e15fe0bc..856e079bb3 100644 --- a/pkg/frost/signing/native_adapter_build_frost_native_test.go +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -45,12 +45,22 @@ func (mneb *mockNativeExecutionBridge) RegisterUnmarshallers( mneb.lastChannel = channel } +func staticNativeBridgeProvider( + bridge NativeExecutionBridge, +) func() NativeExecutionBridge { + return func() NativeExecutionBridge { + return bridge + } +} + func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() + UnregisterNativeExecutionBridge() RegisterNativeExecutionAdapterForBuild() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) + t.Cleanup(UnregisterNativeExecutionBridge) err := SetExecutionBackendByName("native") if err != nil { @@ -100,6 +110,29 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { err, ) } + + registeredBridge := &mockNativeExecutionBridge{ + available: true, + result: &Result{}, + } + + err = RegisterNativeExecutionBridge(registeredBridge) + if err != nil { + t.Fatalf("failed registering native execution bridge: [%v]", err) + } + + err = SetExecutionBackendByName("ffi") + if err != nil { + t.Fatalf("unexpected strict ffi backend config error: [%v]", err) + } + + if CurrentExecutionBackendName() != NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name for strict ffi config\nexpected: [%s]\nactual: [%s]", + NativeExecutionBackendName, + CurrentExecutionBackendName(), + ) + } } func TestBuildTaggedNativeExecutionAdapter_Execute_UsesNativeBridgeWhenAvailable( @@ -114,8 +147,8 @@ func TestBuildTaggedNativeExecutionAdapter_Execute_UsesNativeBridgeWhenAvailable fallback := &mockExecutionBackend{name: "fallback"} adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } result, err := adapter.Execute(context.Background(), nil, &Request{}) @@ -154,8 +187,8 @@ func TestBuildTaggedNativeExecutionAdapter_Execute_FallsBackWhenBridgeUnavailabl } adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } result, err := adapter.Execute(context.Background(), nil, &Request{}) @@ -195,8 +228,8 @@ func TestBuildTaggedNativeExecutionAdapter_Execute_FallsBackOnUnavailableBridgeE } adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } result, err := adapter.Execute(context.Background(), nil, &Request{}) @@ -233,8 +266,8 @@ func TestBuildTaggedNativeExecutionAdapter_Execute_ReturnsBridgeError( fallback := &mockExecutionBackend{name: "fallback"} adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } _, err := adapter.Execute(context.Background(), nil, &Request{}) @@ -273,8 +306,8 @@ func TestBuildTaggedNativeExecutionAdapter_Execute_StrictModeNoFallbackWhenUnava } adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } _, err := adapter.Execute(context.Background(), nil, &Request{}) @@ -314,8 +347,8 @@ func TestBuildTaggedNativeExecutionAdapter_Execute_StrictModeNoFallbackOnUnavail } adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } _, err := adapter.Execute(context.Background(), nil, &Request{}) @@ -346,8 +379,8 @@ func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_UsesNativeWhenA fallback := &mockExecutionBackend{name: "fallback"} adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } adapter.RegisterUnmarshallers(nil) @@ -377,8 +410,8 @@ func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_FallsBackWhenUn fallback := &mockExecutionBackend{name: "fallback"} adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } adapter.RegisterUnmarshallers(nil) @@ -413,8 +446,8 @@ func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_StrictModeNoFal fallback := &mockExecutionBackend{name: "fallback"} adapter := &buildTaggedNativeExecutionAdapter{ - nativeBridge: bridge, - fallback: fallback, + nativeBridgeProvider: staticNativeBridgeProvider(bridge), + fallback: fallback, } adapter.RegisterUnmarshallers(nil) diff --git a/pkg/frost/signing/native_adapter_registration_frost_native.go b/pkg/frost/signing/native_adapter_registration_frost_native.go index 56a00a8882..2d57ab6cc3 100644 --- a/pkg/frost/signing/native_adapter_registration_frost_native.go +++ b/pkg/frost/signing/native_adapter_registration_frost_native.go @@ -20,8 +20,8 @@ import ( // - `native`: fallback to legacy bridge when native cryptography is unavailable // - `ffi`: no fallback; native cryptographic execution is required type buildTaggedNativeExecutionAdapter struct { - nativeBridge nativeExecutionBridge - fallback ExecutionBackend + nativeBridgeProvider func() NativeExecutionBridge + fallback ExecutionBackend } func registerNativeExecutionAdapterForBuild() { @@ -33,13 +33,22 @@ func registerNativeExecutionAdapterForBuild() { func newBuildTaggedNativeExecutionAdapter() *buildTaggedNativeExecutionAdapter { return &buildTaggedNativeExecutionAdapter{ - nativeBridge: newNativeExecutionBridge(), - fallback: newLegacyExecutionBackend(), + nativeBridgeProvider: newNativeExecutionBridge, + fallback: newLegacyExecutionBackend(), } } func (btnea *buildTaggedNativeExecutionAdapter) NativeExecutionAvailable() bool { - return btnea.nativeBridge != nil && btnea.nativeBridge.IsAvailable() + nativeBridge := btnea.currentNativeBridge() + return nativeBridge != nil && nativeBridge.IsAvailable() +} + +func (btnea *buildTaggedNativeExecutionAdapter) currentNativeBridge() NativeExecutionBridge { + if btnea.nativeBridgeProvider == nil { + return nil + } + + return btnea.nativeBridgeProvider() } func (btnea *buildTaggedNativeExecutionAdapter) Execute( @@ -47,8 +56,9 @@ func (btnea *buildTaggedNativeExecutionAdapter) Execute( logger log.StandardLogger, request *Request, ) (*Result, error) { - if btnea.nativeBridge != nil && btnea.nativeBridge.IsAvailable() { - result, err := btnea.nativeBridge.Execute(ctx, logger, request) + nativeBridge := btnea.currentNativeBridge() + if nativeBridge != nil && nativeBridge.IsAvailable() { + result, err := nativeBridge.Execute(ctx, logger, request) if err == nil { return result, nil } @@ -83,8 +93,9 @@ func (btnea *buildTaggedNativeExecutionAdapter) Execute( func (btnea *buildTaggedNativeExecutionAdapter) RegisterUnmarshallers( channel net.BroadcastChannel, ) { - if btnea.nativeBridge != nil && btnea.nativeBridge.IsAvailable() { - btnea.nativeBridge.RegisterUnmarshallers(channel) + nativeBridge := btnea.currentNativeBridge() + if nativeBridge != nil && nativeBridge.IsAvailable() { + nativeBridge.RegisterUnmarshallers(channel) return } diff --git a/pkg/frost/signing/native_bridge.go b/pkg/frost/signing/native_bridge.go index df65d89fc0..3f61a9f1b4 100644 --- a/pkg/frost/signing/native_bridge.go +++ b/pkg/frost/signing/native_bridge.go @@ -19,12 +19,12 @@ var ( ) ) -// nativeExecutionBridge defines a native cryptographic execution entrypoint +// NativeExecutionBridge defines a native cryptographic execution entrypoint // used by the frost_native adapter. // // The current implementation returns ErrNativeCryptographyUnavailable. Future // FFI-backed integrations should provide an available bridge implementation. -type nativeExecutionBridge interface { +type NativeExecutionBridge interface { IsAvailable() bool Execute( ctx context.Context, @@ -34,7 +34,43 @@ type nativeExecutionBridge interface { RegisterUnmarshallers(channel net.BroadcastChannel) } -func newNativeExecutionBridge() nativeExecutionBridge { +// RegisterNativeExecutionBridge registers a native execution bridge for +// frost_native adapter routing. +func RegisterNativeExecutionBridge(bridge NativeExecutionBridge) error { + if bridge == nil { + return errors.New("native execution bridge is nil") + } + + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + registeredNativeExecBridge = bridge + + return nil +} + +// UnregisterNativeExecutionBridge clears the registered native execution +// bridge. +func UnregisterNativeExecutionBridge() { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + registeredNativeExecBridge = nil +} + +func currentNativeExecutionBridge() NativeExecutionBridge { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return registeredNativeExecBridge +} + +func newNativeExecutionBridge() NativeExecutionBridge { + bridge := currentNativeExecutionBridge() + if bridge != nil { + return bridge + } + return &unlinkedNativeExecutionBridge{} } From d0076350514ee4fb5afa5c2bdd86ff5322262018 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 16:06:44 -0600 Subject: [PATCH 18/96] Register build-tagged bridge for strict ffi path --- .../native_adapter_build_frost_native_test.go | 15 ++++++ ...ative_adapter_registration_frost_native.go | 7 ++- .../signing/native_bridge_frost_native.go | 51 +++++++++++++++++++ ...igning_native_backend_frost_native_test.go | 30 ++++++++++- 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 pkg/frost/signing/native_bridge_frost_native.go diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go index 856e079bb3..d99720ff62 100644 --- a/pkg/frost/signing/native_adapter_build_frost_native_test.go +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -90,6 +90,21 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { ) } + err = SetExecutionBackendByName("ffi") + if err != nil { + t.Fatalf("unexpected strict ffi backend config error: [%v]", err) + } + + if CurrentExecutionBackendName() != NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name for strict ffi config\nexpected: [%s]\nactual: [%s]", + NativeExecutionBackendName, + CurrentExecutionBackendName(), + ) + } + + UnregisterNativeExecutionBridge() + err = SetExecutionBackendByName("ffi") if err == nil { t.Fatal("expected strict ffi backend unavailable error") diff --git a/pkg/frost/signing/native_adapter_registration_frost_native.go b/pkg/frost/signing/native_adapter_registration_frost_native.go index 2d57ab6cc3..86c94bd3f4 100644 --- a/pkg/frost/signing/native_adapter_registration_frost_native.go +++ b/pkg/frost/signing/native_adapter_registration_frost_native.go @@ -25,7 +25,12 @@ type buildTaggedNativeExecutionAdapter struct { } func registerNativeExecutionAdapterForBuild() { - err := RegisterNativeExecutionAdapter(newBuildTaggedNativeExecutionAdapter()) + err := RegisterNativeExecutionBridge(newBuildTaggedNativeExecutionBridge()) + if err != nil { + panic(fmt.Sprintf("failed to register build-tagged native bridge: [%v]", err)) + } + + err = RegisterNativeExecutionAdapter(newBuildTaggedNativeExecutionAdapter()) if err != nil { panic(fmt.Sprintf("failed to register build-tagged native adapter: [%v]", err)) } diff --git a/pkg/frost/signing/native_bridge_frost_native.go b/pkg/frost/signing/native_bridge_frost_native.go new file mode 100644 index 0000000000..f06229f8d9 --- /dev/null +++ b/pkg/frost/signing/native_bridge_frost_native.go @@ -0,0 +1,51 @@ +//go:build frost_native + +package signing + +import ( + "context" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" +) + +// buildTaggedNativeExecutionBridge is a transitional native bridge registered +// for frost_native builds. +// +// Until a real FFI-backed bridge is linked, this bridge delegates to the +// legacy signing backend while still surfacing native-bridge availability. +type buildTaggedNativeExecutionBridge struct { + delegate ExecutionBackend +} + +func newBuildTaggedNativeExecutionBridge() NativeExecutionBridge { + return &buildTaggedNativeExecutionBridge{ + delegate: newLegacyExecutionBackend(), + } +} + +func (btneb *buildTaggedNativeExecutionBridge) IsAvailable() bool { + return btneb.delegate != nil +} + +func (btneb *buildTaggedNativeExecutionBridge) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + if btneb.delegate == nil { + return nil, ErrNativeCryptographyUnavailable + } + + return btneb.delegate.Execute(ctx, logger, request) +} + +func (btneb *buildTaggedNativeExecutionBridge) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + if btneb.delegate == nil { + return + } + + btneb.delegate.RegisterUnmarshallers(channel) +} diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index 8fffd9a5f5..d99745ff66 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -12,12 +12,38 @@ import ( frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" ) -func TestConfigureFrostSigningBackend_FFIStrictUnavailable_BuildAdapter(t *testing.T) { +func TestConfigureFrostSigningBackend_FFIStrictConfigured_BuildAdapter(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.UnregisterNativeExecutionBridge() frostsigning.RegisterNativeExecutionAdapterForBuild() t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + + err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + if err != nil { + t.Fatalf("unexpected strict ffi backend configuration error: [%v]", err) + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } +} + +func TestConfigureFrostSigningBackend_FFIStrictUnavailable_NoBridge(t *testing.T) { + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.UnregisterNativeExecutionBridge() + frostsigning.RegisterNativeExecutionAdapterForBuild() + frostsigning.UnregisterNativeExecutionBridge() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) if err == nil { @@ -46,9 +72,11 @@ func TestSigningExecutor_Sign_NativeBackend(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.UnregisterNativeExecutionBridge() frostsigning.RegisterNativeExecutionAdapterForBuild() t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) err := configureFrostSigningBackend(Config{FrostSigningBackend: "native"}) if err != nil { From 8e23f5ef01c5ddaeed99f5f45f28dafc7958b8a3 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 16:31:01 -0600 Subject: [PATCH 19/96] frost/signing: add ffi executor registration path --- pkg/frost/signing/backend.go | 8 +- pkg/frost/signing/backend_test.go | 12 + .../native_adapter_build_frost_native_test.go | 12 +- .../signing/native_bridge_frost_native.go | 59 ++++- .../native_bridge_frost_native_test.go | 213 ++++++++++++++++++ pkg/frost/signing/native_ffi_executor.go | 51 +++++ ...igning_native_backend_frost_native_test.go | 32 ++- 7 files changed, 374 insertions(+), 13 deletions(-) create mode 100644 pkg/frost/signing/native_bridge_frost_native_test.go create mode 100644 pkg/frost/signing/native_ffi_executor.go diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index aca70a4b87..38e19dfdea 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -34,13 +34,15 @@ var ( "native FROST signing backend is unavailable in this build", ) - // executionBackend, nativeExecutionAdapter, and registeredNativeExecBridge - // are process-global runtime state. Tests mutating this state must run - // sequentially; do not use t.Parallel in such tests. + // executionBackend, nativeExecutionAdapter, registeredNativeExecBridge, and + // nativeExecutionFFIExecutor are process-global runtime state. Tests + // mutating this state must run sequentially; do not use t.Parallel in such + // tests. executionBackendMutex sync.RWMutex executionBackend ExecutionBackend = newLegacyExecutionBackend() nativeExecutionAdapter NativeExecutionAdapter registeredNativeExecBridge NativeExecutionBridge + nativeExecutionFFIExecutor NativeExecutionFFIExecutor nativeExecutionMode = nativeExecutionModeFallbackAllowed ) diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go index 067737a594..6cd8a47826 100644 --- a/pkg/frost/signing/backend_test.go +++ b/pkg/frost/signing/backend_test.go @@ -103,9 +103,11 @@ func TestSetExecutionBackendByName(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) t.Cleanup(UnregisterNativeExecutionBridge) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) if err := SetExecutionBackendByName(""); err != nil { t.Fatalf("unexpected default backend config error: [%v]", err) @@ -169,9 +171,11 @@ func TestSetExecutionBackendByName_NativeAdapterRegistered(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) t.Cleanup(UnregisterNativeExecutionBridge) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) expectedResult := &Result{Signature: &frost.Signature{}} adapter := &mockNativeExecutionAdapter{ @@ -247,9 +251,11 @@ func TestSetExecutionBackendByName_FFIStrictAvailabilityCheck(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) t.Cleanup(UnregisterNativeExecutionBridge) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) adapter := &mockNativeExecutionAdapterWithAvailability{ mockNativeExecutionAdapter: &mockNativeExecutionAdapter{}, @@ -303,6 +309,12 @@ func TestRegisterNativeExecutionBridge_Nil(t *testing.T) { } } +func TestRegisterNativeExecutionFFIExecutor_Nil(t *testing.T) { + if err := RegisterNativeExecutionFFIExecutor(nil); err == nil { + t.Fatal("expected nil native FFI executor error") + } +} + func TestExecute_DelegatesToCurrentBackend(t *testing.T) { ResetExecutionBackend() t.Cleanup(ResetExecutionBackend) diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go index d99720ff62..0bf861dba2 100644 --- a/pkg/frost/signing/native_adapter_build_frost_native_test.go +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -91,15 +91,15 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { } err = SetExecutionBackendByName("ffi") - if err != nil { - t.Fatalf("unexpected strict ffi backend config error: [%v]", err) + if err == nil { + t.Fatal("expected strict ffi backend unavailable error") } - if CurrentExecutionBackendName() != NativeExecutionBackendName { + if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { t.Fatalf( - "unexpected backend name for strict ffi config\nexpected: [%s]\nactual: [%s]", - NativeExecutionBackendName, - CurrentExecutionBackendName(), + "unexpected ffi backend error\nexpected: [%v]\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + err, ) } diff --git a/pkg/frost/signing/native_bridge_frost_native.go b/pkg/frost/signing/native_bridge_frost_native.go index f06229f8d9..1cb5e9d186 100644 --- a/pkg/frost/signing/native_bridge_frost_native.go +++ b/pkg/frost/signing/native_bridge_frost_native.go @@ -4,6 +4,8 @@ package signing import ( "context" + "errors" + "fmt" "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/net" @@ -15,17 +17,31 @@ import ( // Until a real FFI-backed bridge is linked, this bridge delegates to the // legacy signing backend while still surfacing native-bridge availability. type buildTaggedNativeExecutionBridge struct { - delegate ExecutionBackend + ffiExecutorProvider func() NativeExecutionFFIExecutor + delegate ExecutionBackend } func newBuildTaggedNativeExecutionBridge() NativeExecutionBridge { return &buildTaggedNativeExecutionBridge{ - delegate: newLegacyExecutionBackend(), + ffiExecutorProvider: currentNativeExecutionFFIExecutor, + delegate: newLegacyExecutionBackend(), } } func (btneb *buildTaggedNativeExecutionBridge) IsAvailable() bool { - return btneb.delegate != nil + if btneb.currentFFIExecutor() != nil { + return true + } + + return nativeExecutionFallbackAllowed() && btneb.delegate != nil +} + +func (btneb *buildTaggedNativeExecutionBridge) currentFFIExecutor() NativeExecutionFFIExecutor { + if btneb.ffiExecutorProvider == nil { + return nil + } + + return btneb.ffiExecutorProvider() } func (btneb *buildTaggedNativeExecutionBridge) Execute( @@ -33,6 +49,33 @@ func (btneb *buildTaggedNativeExecutionBridge) Execute( logger log.StandardLogger, request *Request, ) (*Result, error) { + ffiExecutor := btneb.currentFFIExecutor() + if ffiExecutor != nil { + result, err := ffiExecutor.Execute(ctx, logger, request) + if err == nil { + return result, nil + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + return nil, fmt.Errorf("native FFI executor execution failed: [%w]", err) + } + + if !nativeExecutionFallbackAllowed() { + return nil, err + } + + if logger != nil { + logger.Warnf( + "native FFI executor unavailable; falling back to legacy bridge backend: [%v]", + err, + ) + } + } + + if !nativeExecutionFallbackAllowed() { + return nil, ErrNativeCryptographyUnavailable + } + if btneb.delegate == nil { return nil, ErrNativeCryptographyUnavailable } @@ -43,6 +86,16 @@ func (btneb *buildTaggedNativeExecutionBridge) Execute( func (btneb *buildTaggedNativeExecutionBridge) RegisterUnmarshallers( channel net.BroadcastChannel, ) { + ffiExecutor := btneb.currentFFIExecutor() + if ffiExecutor != nil { + ffiExecutor.RegisterUnmarshallers(channel) + return + } + + if !nativeExecutionFallbackAllowed() { + return + } + if btneb.delegate == nil { return } diff --git a/pkg/frost/signing/native_bridge_frost_native_test.go b/pkg/frost/signing/native_bridge_frost_native_test.go new file mode 100644 index 0000000000..dc13db5cfc --- /dev/null +++ b/pkg/frost/signing/native_bridge_frost_native_test.go @@ -0,0 +1,213 @@ +//go:build frost_native + +package signing + +import ( + "context" + "errors" + "testing" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" +) + +type mockNativeExecutionFFIExecutor struct { + executeCalls int + lastRequest *Request + result *Result + err error + + registerUnmarshallersCalls int + lastChannel net.BroadcastChannel +} + +func (mnefe *mockNativeExecutionFFIExecutor) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + mnefe.executeCalls++ + mnefe.lastRequest = request + return mnefe.result, mnefe.err +} + +func (mnefe *mockNativeExecutionFFIExecutor) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + mnefe.registerUnmarshallersCalls++ + mnefe.lastChannel = channel +} + +func staticNativeFFIExecutorProvider( + executor NativeExecutionFFIExecutor, +) func() NativeExecutionFFIExecutor { + return func() NativeExecutionFFIExecutor { + return executor + } +} + +func TestBuildTaggedNativeExecutionBridge_Execute_UsesFFIExecutor( + t *testing.T, +) { + expectedResult := &Result{} + ffiExecutor := &mockNativeExecutionFFIExecutor{ + result: expectedResult, + } + + fallback := &mockExecutionBackend{ + name: "fallback", + result: &Result{}, + } + + bridge := &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: staticNativeFFIExecutorProvider(ffiExecutor), + delegate: fallback, + } + + result, err := bridge.Execute(context.Background(), nil, &Request{}) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if result != expectedResult { + t.Fatalf( + "unexpected result\nexpected: [%+v]\nactual: [%+v]", + expectedResult, + result, + ) + } + + if ffiExecutor.executeCalls != 1 { + t.Fatalf( + "unexpected ffi executor execute calls count: [%d]", + ffiExecutor.executeCalls, + ) + } + + if fallback.executeCalls != 0 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionBridge_Execute_StrictNoFallbackWithoutFFIExecutor( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeStrict) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + fallback := &mockExecutionBackend{ + name: "fallback", + result: &Result{}, + } + + bridge := &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: staticNativeFFIExecutorProvider(nil), + delegate: fallback, + } + + _, err := bridge.Execute(context.Background(), nil, &Request{}) + if err == nil { + t.Fatal("expected execute error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected execute error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if fallback.executeCalls != 0 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionBridge_Execute_FallsBackWithoutFFIExecutor( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + + expectedResult := &Result{} + fallback := &mockExecutionBackend{ + name: "fallback", + result: expectedResult, + } + + bridge := &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: staticNativeFFIExecutorProvider(nil), + delegate: fallback, + } + + result, err := bridge.Execute(context.Background(), nil, &Request{}) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if result != expectedResult { + t.Fatalf( + "unexpected result\nexpected: [%+v]\nactual: [%+v]", + expectedResult, + result, + ) + } + + if fallback.executeCalls != 1 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionBridge_RegisterUnmarshallers_UsesFFIExecutor( + t *testing.T, +) { + ffiExecutor := &mockNativeExecutionFFIExecutor{} + fallback := &mockExecutionBackend{name: "fallback"} + + bridge := &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: staticNativeFFIExecutorProvider(ffiExecutor), + delegate: fallback, + } + + bridge.RegisterUnmarshallers(nil) + + if ffiExecutor.registerUnmarshallersCalls != 1 { + t.Fatalf( + "unexpected ffi executor register unmarshallers calls count: [%d]", + ffiExecutor.registerUnmarshallersCalls, + ) + } + + if fallback.registerUnmarshallersCalls != 0 { + t.Fatalf( + "unexpected fallback register unmarshallers calls count: [%d]", + fallback.registerUnmarshallersCalls, + ) + } +} + +func TestBuildTaggedNativeExecutionBridge_RegisterUnmarshallers_StrictNoFallback( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeStrict) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + fallback := &mockExecutionBackend{name: "fallback"} + + bridge := &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: staticNativeFFIExecutorProvider(nil), + delegate: fallback, + } + + bridge.RegisterUnmarshallers(nil) + + if fallback.registerUnmarshallersCalls != 0 { + t.Fatalf( + "unexpected fallback register unmarshallers calls count: [%d]", + fallback.registerUnmarshallersCalls, + ) + } +} diff --git a/pkg/frost/signing/native_ffi_executor.go b/pkg/frost/signing/native_ffi_executor.go new file mode 100644 index 0000000000..fe45850d9f --- /dev/null +++ b/pkg/frost/signing/native_ffi_executor.go @@ -0,0 +1,51 @@ +package signing + +import ( + "context" + "errors" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" +) + +// NativeExecutionFFIExecutor is a bridge to the native/FFI signing engine. +// This executor is intended to run FROST-native cryptographic execution. +type NativeExecutionFFIExecutor interface { + Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, + ) (*Result, error) + RegisterUnmarshallers(channel net.BroadcastChannel) +} + +// RegisterNativeExecutionFFIExecutor registers a native FFI executor used by +// build-tagged bridges. +func RegisterNativeExecutionFFIExecutor(executor NativeExecutionFFIExecutor) error { + if executor == nil { + return errors.New("native execution FFI executor is nil") + } + + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeExecutionFFIExecutor = executor + + return nil +} + +// UnregisterNativeExecutionFFIExecutor clears the native FFI executor +// registration. +func UnregisterNativeExecutionFFIExecutor() { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeExecutionFFIExecutor = nil +} + +func currentNativeExecutionFFIExecutor() NativeExecutionFFIExecutor { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return nativeExecutionFFIExecutor +} diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index d99745ff66..1765a52471 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -9,19 +9,44 @@ import ( "math/big" "testing" + "github.com/ipfs/go-log/v2" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/net" ) +type noopNativeExecutionFFIExecutor struct{} + +func (nnefe *noopNativeExecutionFFIExecutor) Execute( + ctx context.Context, + logger log.StandardLogger, + request *frostsigning.Request, +) (*frostsigning.Result, error) { + return nil, nil +} + +func (nnefe *noopNativeExecutionFFIExecutor) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { +} + func TestConfigureFrostSigningBackend_FFIStrictConfigured_BuildAdapter(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() frostsigning.UnregisterNativeExecutionBridge() + frostsigning.UnregisterNativeExecutionFFIExecutor() frostsigning.RegisterNativeExecutionAdapterForBuild() + err := frostsigning.RegisterNativeExecutionFFIExecutor( + &noopNativeExecutionFFIExecutor{}, + ) + if err != nil { + t.Fatalf("unexpected native FFI executor registration error: [%v]", err) + } t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) - err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + err = configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) if err != nil { t.Fatalf("unexpected strict ffi backend configuration error: [%v]", err) } @@ -39,11 +64,14 @@ func TestConfigureFrostSigningBackend_FFIStrictUnavailable_NoBridge(t *testing.T frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() frostsigning.UnregisterNativeExecutionBridge() + frostsigning.UnregisterNativeExecutionFFIExecutor() frostsigning.RegisterNativeExecutionAdapterForBuild() frostsigning.UnregisterNativeExecutionBridge() + frostsigning.UnregisterNativeExecutionFFIExecutor() t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) if err == nil { @@ -73,10 +101,12 @@ func TestSigningExecutor_Sign_NativeBackend(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() frostsigning.UnregisterNativeExecutionBridge() + frostsigning.UnregisterNativeExecutionFFIExecutor() frostsigning.RegisterNativeExecutionAdapterForBuild() t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) err := configureFrostSigningBackend(Config{FrostSigningBackend: "native"}) if err != nil { From ed642b294494b2286ad30fb962df499f24974795 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 17:15:50 -0600 Subject: [PATCH 20/96] frost/signing: restore mode on failed backend selection --- pkg/frost/signing/backend.go | 27 +++- pkg/frost/signing/backend_test.go | 122 +++++++++++++- .../native_bridge_frost_native_test.go | 152 ++++++++++++++++++ ...igning_native_backend_frost_native_test.go | 2 + 4 files changed, 299 insertions(+), 4 deletions(-) diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 38e19dfdea..48ff3489b2 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -112,26 +112,49 @@ func SetExecutionBackendByName(name string) error { ResetExecutionBackend() return nil case "native": + previousMode := currentNativeExecutionMode() setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + nativeBackend, err := currentNativeExecutionBackend() if err != nil { + setNativeExecutionMode(previousMode) + return err + } + + if err := SetExecutionBackend(nativeBackend); err != nil { + setNativeExecutionMode(previousMode) return err } - return SetExecutionBackend(nativeBackend) + return nil case "ffi": + previousMode := currentNativeExecutionMode() setNativeExecutionMode(nativeExecutionModeStrict) + nativeBackend, err := currentNativeExecutionBackend() if err != nil { + setNativeExecutionMode(previousMode) return err } - return SetExecutionBackend(nativeBackend) + if err := SetExecutionBackend(nativeBackend); err != nil { + setNativeExecutionMode(previousMode) + return err + } + + return nil default: return fmt.Errorf("unknown FROST signing backend: [%s]", name) } } +func currentNativeExecutionMode() nativeExecutionModeValue { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return nativeExecutionMode +} + func setNativeExecutionMode(mode nativeExecutionModeValue) { executionBackendMutex.Lock() defer executionBackendMutex.Unlock() diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go index 6cd8a47826..3a20dac2c9 100644 --- a/pkg/frost/signing/backend_test.go +++ b/pkg/frost/signing/backend_test.go @@ -157,8 +157,17 @@ func TestSetExecutionBackendByName(t *testing.T) { err, ) } - if nativeExecutionFallbackAllowed() { - t.Fatal("expected strict mode for ffi backend selection") + if !nativeExecutionFallbackAllowed() { + t.Fatal( + "expected previous fallback-allowed mode after failed ffi backend selection", + ) + } + if CurrentExecutionBackendName() != legacyExecutionBackendName { + t.Fatalf( + "unexpected backend name after failed ffi config\\nexpected: [%s]\\nactual: [%s]", + legacyExecutionBackendName, + CurrentExecutionBackendName(), + ) } err = SetExecutionBackendByName("unknown") @@ -167,6 +176,115 @@ func TestSetExecutionBackendByName(t *testing.T) { } } +func TestSetExecutionBackendByName_NativeFailureRestoresPreviousMode( + t *testing.T, +) { + ResetExecutionBackend() + UnregisterNativeExecutionAdapter() + UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(ResetExecutionBackend) + t.Cleanup(UnregisterNativeExecutionAdapter) + t.Cleanup(UnregisterNativeExecutionBridge) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + setNativeExecutionMode(nativeExecutionModeStrict) + if nativeExecutionFallbackAllowed() { + t.Fatal("expected strict mode before failed native backend selection") + } + + err := SetExecutionBackendByName("native") + if err == nil { + t.Fatal("expected native backend unavailable error") + } + if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected native backend error\\nexpected: [%v]\\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + err, + ) + } + + if nativeExecutionFallbackAllowed() { + t.Fatal("expected strict mode to be restored after failed native selection") + } + if CurrentExecutionBackendName() != legacyExecutionBackendName { + t.Fatalf( + "unexpected backend name after failed native config\\nexpected: [%s]\\nactual: [%s]", + legacyExecutionBackendName, + CurrentExecutionBackendName(), + ) + } +} + +func TestSetExecutionBackendByName_FFIFailurePreservesNativeModeAndBackend( + t *testing.T, +) { + ResetExecutionBackend() + UnregisterNativeExecutionAdapter() + UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(ResetExecutionBackend) + t.Cleanup(UnregisterNativeExecutionAdapter) + t.Cleanup(UnregisterNativeExecutionBridge) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + adapter := &mockNativeExecutionAdapterWithAvailability{ + mockNativeExecutionAdapter: &mockNativeExecutionAdapter{}, + nativeExecutionAvailable: false, + } + + if err := RegisterNativeExecutionAdapter(adapter); err != nil { + t.Fatalf("failed registering native execution adapter: [%v]", err) + } + + if err := SetExecutionBackendByName("native"); err != nil { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + if !nativeExecutionFallbackAllowed() { + t.Fatal("expected fallback-allowed mode after native backend selection") + } + if CurrentExecutionBackendName() != nativeExecutionBackendName { + t.Fatalf( + "unexpected backend name for native config\\nexpected: [%s]\\nactual: [%s]", + nativeExecutionBackendName, + CurrentExecutionBackendName(), + ) + } + + err := SetExecutionBackendByName("ffi") + if err == nil { + t.Fatal("expected ffi backend unavailable error") + } + if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected ffi backend error\\nexpected: [%v]\\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + err, + ) + } + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected strict-mode availability error\\nexpected: [%v]\\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if !nativeExecutionFallbackAllowed() { + t.Fatal( + "expected fallback-allowed mode to be preserved after failed ffi selection", + ) + } + if CurrentExecutionBackendName() != nativeExecutionBackendName { + t.Fatalf( + "unexpected backend name after failed ffi config\\nexpected: [%s]\\nactual: [%s]", + nativeExecutionBackendName, + CurrentExecutionBackendName(), + ) + } +} + func TestSetExecutionBackendByName_NativeAdapterRegistered(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() diff --git a/pkg/frost/signing/native_bridge_frost_native_test.go b/pkg/frost/signing/native_bridge_frost_native_test.go index dc13db5cfc..0608b743ac 100644 --- a/pkg/frost/signing/native_bridge_frost_native_test.go +++ b/pkg/frost/signing/native_bridge_frost_native_test.go @@ -5,6 +5,7 @@ package signing import ( "context" "errors" + "strings" "testing" "github.com/ipfs/go-log/v2" @@ -159,6 +160,157 @@ func TestBuildTaggedNativeExecutionBridge_Execute_FallsBackWithoutFFIExecutor( } } +func TestBuildTaggedNativeExecutionBridge_Execute_StrictNoFallbackOnFFIUnavailableError( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeStrict) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + ffiExecutor := &mockNativeExecutionFFIExecutor{ + err: ErrNativeCryptographyUnavailable, + } + fallback := &mockExecutionBackend{ + name: "fallback", + result: &Result{}, + } + + bridge := &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: staticNativeFFIExecutorProvider(ffiExecutor), + delegate: fallback, + } + + _, err := bridge.Execute(context.Background(), nil, &Request{}) + if err == nil { + t.Fatal("expected execute error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected execute error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if ffiExecutor.executeCalls != 1 { + t.Fatalf( + "unexpected ffi executor execute calls count: [%d]", + ffiExecutor.executeCalls, + ) + } + + if fallback.executeCalls != 0 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionBridge_Execute_FallsBackOnFFIUnavailableError( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + expectedResult := &Result{} + ffiExecutor := &mockNativeExecutionFFIExecutor{ + err: ErrNativeCryptographyUnavailable, + } + fallback := &mockExecutionBackend{ + name: "fallback", + result: expectedResult, + } + + bridge := &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: staticNativeFFIExecutorProvider(ffiExecutor), + delegate: fallback, + } + + result, err := bridge.Execute(context.Background(), nil, &Request{}) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if result != expectedResult { + t.Fatalf( + "unexpected result\nexpected: [%+v]\nactual: [%+v]", + expectedResult, + result, + ) + } + + if ffiExecutor.executeCalls != 1 { + t.Fatalf( + "unexpected ffi executor execute calls count: [%d]", + ffiExecutor.executeCalls, + ) + } + + if fallback.executeCalls != 1 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + +func TestBuildTaggedNativeExecutionBridge_Execute_NoFallbackOnFFIExecutionError( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + ffiExecutionError := errors.New("ffi executor crashed") + ffiExecutor := &mockNativeExecutionFFIExecutor{ + err: ffiExecutionError, + } + fallback := &mockExecutionBackend{ + name: "fallback", + result: &Result{}, + } + + bridge := &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: staticNativeFFIExecutorProvider(ffiExecutor), + delegate: fallback, + } + + _, err := bridge.Execute(context.Background(), nil, &Request{}) + if err == nil { + t.Fatal("expected execute error") + } + + if !errors.Is(err, ffiExecutionError) { + t.Fatalf( + "unexpected execute error\nexpected to wrap: [%v]\nactual: [%v]", + ffiExecutionError, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected availability error wrapping for non-availability failure: [%v]", + err, + ) + } + + if !strings.Contains(err.Error(), "native FFI executor execution failed") { + t.Fatalf("unexpected error message: [%v]", err) + } + + if ffiExecutor.executeCalls != 1 { + t.Fatalf( + "unexpected ffi executor execute calls count: [%d]", + ffiExecutor.executeCalls, + ) + } + + if fallback.executeCalls != 0 { + t.Fatalf("unexpected fallback execute calls count: [%d]", fallback.executeCalls) + } +} + func TestBuildTaggedNativeExecutionBridge_RegisterUnmarshallers_UsesFFIExecutor( t *testing.T, ) { diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index 1765a52471..b1507a65e0 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -66,6 +66,8 @@ func TestConfigureFrostSigningBackend_FFIStrictUnavailable_NoBridge(t *testing.T frostsigning.UnregisterNativeExecutionBridge() frostsigning.UnregisterNativeExecutionFFIExecutor() frostsigning.RegisterNativeExecutionAdapterForBuild() + // Remove build-registered bridge and executor to exercise strict ffi + // configuration when no native cryptography path is available. frostsigning.UnregisterNativeExecutionBridge() frostsigning.UnregisterNativeExecutionFFIExecutor() t.Cleanup(frostsigning.ResetExecutionBackend) From 74e894ccc0c1a37a88049bf350b8d1ec54caceef Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 17:30:01 -0600 Subject: [PATCH 21/96] frost/signing: decouple request signer material from tecdsa share --- pkg/frost/signing/legacy_backend.go | 7 +- pkg/frost/signing/request.go | 39 +++++++++ pkg/frost/signing/request_test.go | 120 ++++++++++++++++++++++++++++ pkg/frost/signing/signing.go | 1 + 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 pkg/frost/signing/request_test.go diff --git a/pkg/frost/signing/legacy_backend.go b/pkg/frost/signing/legacy_backend.go index 456fa05805..57b357ea83 100644 --- a/pkg/frost/signing/legacy_backend.go +++ b/pkg/frost/signing/legacy_backend.go @@ -48,13 +48,18 @@ func (leb *legacyExecutionBackend) Execute( excludedMembersIndexes = request.Attempt.ExcludedMembersIndexes } + privateKeyShare, err := request.LegacyPrivateKeyShare() + if err != nil { + return nil, err + } + legacyResult, err := legacySigning.Execute( ctx, logger, request.Message, request.SessionID, request.MemberIndex, - request.PrivateKeyShare, + privateKeyShare, request.GroupSize, request.DishonestThreshold, excludedMembersIndexes, diff --git a/pkg/frost/signing/request.go b/pkg/frost/signing/request.go index fc94320f0b..2d6eef7052 100644 --- a/pkg/frost/signing/request.go +++ b/pkg/frost/signing/request.go @@ -1,6 +1,7 @@ package signing import ( + "fmt" "math/big" "github.com/keep-network/keep-core/pkg/net" @@ -13,6 +14,11 @@ type Request struct { Message *big.Int SessionID string MemberIndex group.MemberIndex + // SignerMaterial carries backend-specific signer material. + // Legacy backend expects *tecdsa.PrivateKeyShare. + SignerMaterial any + // PrivateKeyShare is a deprecated legacy alias kept for backward + // compatibility while migrating to backend-specific signer material. PrivateKeyShare *tecdsa.PrivateKeyShare GroupSize int DishonestThreshold int @@ -20,3 +26,36 @@ type Request struct { MembershipValidator *group.MembershipValidator Attempt *Attempt } + +// LegacyPrivateKeyShare resolves the tECDSA private key share required by the +// transitional legacy execution backend. +// +// It first checks the deprecated Request.PrivateKeyShare field for backward +// compatibility, and then falls back to Request.SignerMaterial. +func (r *Request) LegacyPrivateKeyShare() (*tecdsa.PrivateKeyShare, error) { + if r == nil { + return nil, fmt.Errorf("request is nil") + } + + if r.PrivateKeyShare != nil { + return r.PrivateKeyShare, nil + } + + if r.SignerMaterial == nil { + return nil, fmt.Errorf("legacy private key share is nil") + } + + privateKeyShare, ok := r.SignerMaterial.(*tecdsa.PrivateKeyShare) + if !ok { + return nil, fmt.Errorf( + "legacy signing material has wrong type: [%T]", + r.SignerMaterial, + ) + } + + if privateKeyShare == nil { + return nil, fmt.Errorf("legacy private key share is nil") + } + + return privateKeyShare, nil +} diff --git a/pkg/frost/signing/request_test.go b/pkg/frost/signing/request_test.go new file mode 100644 index 0000000000..388b998e10 --- /dev/null +++ b/pkg/frost/signing/request_test.go @@ -0,0 +1,120 @@ +package signing + +import ( + "strings" + "testing" + + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestRequest_LegacyPrivateKeyShare_FromDeprecatedField(t *testing.T) { + expected := new(tecdsa.PrivateKeyShare) + + request := &Request{ + PrivateKeyShare: expected, + } + + actual, err := request.LegacyPrivateKeyShare() + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if actual != expected { + t.Fatalf( + "unexpected private key share\nexpected: [%v]\nactual: [%v]", + expected, + actual, + ) + } +} + +func TestRequest_LegacyPrivateKeyShare_FromSignerMaterial(t *testing.T) { + expected := new(tecdsa.PrivateKeyShare) + + request := &Request{ + SignerMaterial: expected, + } + + actual, err := request.LegacyPrivateKeyShare() + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if actual != expected { + t.Fatalf( + "unexpected private key share\nexpected: [%v]\nactual: [%v]", + expected, + actual, + ) + } +} + +func TestRequest_LegacyPrivateKeyShare_NilRequest(t *testing.T) { + _, err := (*Request)(nil).LegacyPrivateKeyShare() + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "request is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "request is nil", + err, + ) + } +} + +func TestRequest_LegacyPrivateKeyShare_NilMaterial(t *testing.T) { + _, err := (&Request{}).LegacyPrivateKeyShare() + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "legacy private key share is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "legacy private key share is nil", + err, + ) + } +} + +func TestRequest_LegacyPrivateKeyShare_WrongMaterialType(t *testing.T) { + request := &Request{ + SignerMaterial: "invalid", + } + + _, err := request.LegacyPrivateKeyShare() + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "legacy signing material has wrong type") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "legacy signing material has wrong type", + err, + ) + } +} + +func TestRequest_LegacyPrivateKeyShare_NilTypedMaterial(t *testing.T) { + var typedNil *tecdsa.PrivateKeyShare + + request := &Request{ + SignerMaterial: typedNil, + } + + _, err := request.LegacyPrivateKeyShare() + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "legacy private key share is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "legacy private key share is nil", + err, + ) + } +} diff --git a/pkg/frost/signing/signing.go b/pkg/frost/signing/signing.go index 593cfbb752..c44ccd8c94 100644 --- a/pkg/frost/signing/signing.go +++ b/pkg/frost/signing/signing.go @@ -35,6 +35,7 @@ func Execute( Message: message, SessionID: sessionID, MemberIndex: memberIndex, + SignerMaterial: privateKeyShare, PrivateKeyShare: privateKeyShare, GroupSize: groupSize, DishonestThreshold: dishonestThreshold, From 656f62ff244ce63d6120d70fad5a9a06c1f6f527 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 17:33:47 -0600 Subject: [PATCH 22/96] frost/tbtc: thread generic signer material through execute request --- pkg/frost/signing/signing.go | 21 +++++- pkg/frost/signing/signing_test.go | 106 ++++++++++++++++++++++++++++++ pkg/tbtc/marshaling.go | 1 + pkg/tbtc/node_test.go | 1 + pkg/tbtc/signing.go | 27 ++++---- pkg/tbtc/wallet.go | 13 ++++ 6 files changed, 155 insertions(+), 14 deletions(-) diff --git a/pkg/frost/signing/signing.go b/pkg/frost/signing/signing.go index c44ccd8c94..3ea4ab3a63 100644 --- a/pkg/frost/signing/signing.go +++ b/pkg/frost/signing/signing.go @@ -41,13 +41,30 @@ func Execute( DishonestThreshold: dishonestThreshold, Channel: channel, MembershipValidator: membershipValidator, - Attempt: cloneAttempt(attempt), + Attempt: attempt, } + return ExecuteRequest(ctx, logger, request) +} + +// ExecuteRequest runs signing using a fully-populated request object. +// It clones mutable request metadata needed for execution safety. +func ExecuteRequest( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + if request == nil { + return nil, fmt.Errorf("request is nil") + } + + clonedRequest := *request + clonedRequest.Attempt = cloneAttempt(request.Attempt) + return currentExecutionBackend().Execute( ctx, logger, - request, + &clonedRequest, ) } diff --git a/pkg/frost/signing/signing_test.go b/pkg/frost/signing/signing_test.go index e0c3bc7b25..f54ff8cfe4 100644 --- a/pkg/frost/signing/signing_test.go +++ b/pkg/frost/signing/signing_test.go @@ -1,9 +1,12 @@ package signing import ( + "context" "math/big" + "reflect" "testing" + "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" ) @@ -75,3 +78,106 @@ func TestFromTECDSASignature_ValidationErrors(t *testing.T) { }) } } + +func TestExecuteRequest_NilRequest(t *testing.T) { + _, err := ExecuteRequest(context.Background(), nil, nil) + if err == nil { + t.Fatal("expected request validation error") + } +} + +func TestExecuteRequest_ClonesAttempt(t *testing.T) { + ResetExecutionBackend() + t.Cleanup(ResetExecutionBackend) + + backend := &mockExecutionBackend{ + name: "mock", + result: &Result{}, + } + + if err := SetExecutionBackend(backend); err != nil { + t.Fatalf("unexpected backend setup error: [%v]", err) + } + + request := &Request{ + Attempt: &Attempt{ + Number: 2, + CoordinatorMemberIndex: 3, + IncludedMembersIndexes: []group.MemberIndex{1, 3, 5}, + ExcludedMembersIndexes: []group.MemberIndex{2, 4}, + }, + } + + if _, err := ExecuteRequest(context.Background(), nil, request); err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if backend.lastRequest == request { + t.Fatal("expected request clone before backend execution") + } + + if backend.lastRequest.Attempt == request.Attempt { + t.Fatal("expected attempt clone before backend execution") + } + + if !reflect.DeepEqual(backend.lastRequest.Attempt, request.Attempt) { + t.Fatalf( + "unexpected attempt clone\nexpected: [%+v]\nactual: [%+v]", + request.Attempt, + backend.lastRequest.Attempt, + ) + } +} + +func TestExecute_PopulatesSignerMaterialAndLegacyAlias(t *testing.T) { + ResetExecutionBackend() + t.Cleanup(ResetExecutionBackend) + + backend := &mockExecutionBackend{ + name: "mock", + result: &Result{}, + } + + if err := SetExecutionBackend(backend); err != nil { + t.Fatalf("unexpected backend setup error: [%v]", err) + } + + privateKeyShare := new(tecdsa.PrivateKeyShare) + + _, err := Execute( + context.Background(), + nil, + big.NewInt(42), + "session-id", + group.MemberIndex(7), + privateKeyShare, + 10, + 3, + nil, + nil, + nil, + ) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if backend.lastRequest == nil { + t.Fatal("expected backend request") + } + + if backend.lastRequest.SignerMaterial != privateKeyShare { + t.Fatalf( + "unexpected signer material\nexpected: [%v]\nactual: [%v]", + privateKeyShare, + backend.lastRequest.SignerMaterial, + ) + } + + if backend.lastRequest.PrivateKeyShare != privateKeyShare { + t.Fatalf( + "unexpected legacy private key share alias\nexpected: [%v]\nactual: [%v]", + privateKeyShare, + backend.lastRequest.PrivateKeyShare, + ) + } +} diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index f96be4f1c1..f9cf56859c 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -84,6 +84,7 @@ func (s *signer) Unmarshal(bytes []byte) error { } s.signingGroupMemberIndex = group.MemberIndex(pbSigner.SigningGroupMemberIndex) s.privateKeyShare = privateKeyShare + s.signerMaterial = privateKeyShare return nil } diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index bedfb30995..c1795dd774 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -491,6 +491,7 @@ func createMockSigner(t *testing.T) *signer { }, signingGroupMemberIndex: group.MemberIndex(1), privateKeyShare: privateKeyShare, + signerMaterial: privateKeyShare, } } diff --git a/pkg/tbtc/signing.go b/pkg/tbtc/signing.go index 7028de4a52..c7c3d33677 100644 --- a/pkg/tbtc/signing.go +++ b/pkg/tbtc/signing.go @@ -346,20 +346,23 @@ func (se *signingExecutor) sign( attempt.number, ) - result, err := signing.Execute( + result, err := signing.ExecuteRequest( attemptCtx, signingAttemptLogger, - message, - sessionID, - signer.signingGroupMemberIndex, - signer.privateKeyShare, - wallet.groupSize(), - wallet.groupDishonestThreshold( - se.groupParameters.HonestThreshold, - ), - se.broadcastChannel, - se.membershipValidator, - attemptInfo, + &signing.Request{ + Message: message, + SessionID: sessionID, + MemberIndex: signer.signingGroupMemberIndex, + SignerMaterial: signer.signingMaterial(), + PrivateKeyShare: signer.privateKeyShare, + GroupSize: wallet.groupSize(), + DishonestThreshold: wallet.groupDishonestThreshold( + se.groupParameters.HonestThreshold, + ), + Channel: se.broadcastChannel, + MembershipValidator: se.membershipValidator, + Attempt: attemptInfo, + }, ) if err != nil { return nil, 0, err diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 321892ac6b..ac19b2baa6 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -780,6 +780,10 @@ type signer struct { // privateKeyShare is the tECDSA private key share required to participate // in the signing process. privateKeyShare *tecdsa.PrivateKeyShare + + // signerMaterial carries backend-specific signer material used by the + // FROST signing runtime. Legacy path falls back to privateKeyShare. + signerMaterial any } // newSigner constructs a new instance of the wallet's signer. @@ -798,9 +802,18 @@ func newSigner( wallet: wallet, signingGroupMemberIndex: signingGroupMemberIndex, privateKeyShare: privateKeyShare, + signerMaterial: privateKeyShare, } } +func (s *signer) signingMaterial() any { + if s.signerMaterial != nil { + return s.signerMaterial + } + + return s.privateKeyShare +} + func (s *signer) String() string { return fmt.Sprintf( "signer with index [%v] of wallet [%s]", From 83bf3af857fd91fb80e97dd30f8a0c81e0c4a9de Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 17:58:20 -0600 Subject: [PATCH 23/96] frost/signing: add native signer material and ffi adapter contract --- .../signing/native_ffi_executor_adapter.go | 117 +++++++ .../native_ffi_executor_adapter_test.go | 308 ++++++++++++++++++ pkg/frost/signing/native_signer_material.go | 90 +++++ .../signing/native_signer_material_test.go | 155 +++++++++ 4 files changed, 670 insertions(+) create mode 100644 pkg/frost/signing/native_ffi_executor_adapter.go create mode 100644 pkg/frost/signing/native_ffi_executor_adapter_test.go create mode 100644 pkg/frost/signing/native_signer_material.go create mode 100644 pkg/frost/signing/native_signer_material_test.go diff --git a/pkg/frost/signing/native_ffi_executor_adapter.go b/pkg/frost/signing/native_ffi_executor_adapter.go new file mode 100644 index 0000000000..e149b563e7 --- /dev/null +++ b/pkg/frost/signing/native_ffi_executor_adapter.go @@ -0,0 +1,117 @@ +package signing + +import ( + "context" + "fmt" + "math/big" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// NativeExecutionFFISigningRequest is the canonical request passed to a native +// FFI signing primitive. +type NativeExecutionFFISigningRequest struct { + Message *big.Int + SessionID string + MemberIndex group.MemberIndex + GroupSize int + DishonestThreshold int + SignerMaterial *NativeSignerMaterial + Attempt *Attempt +} + +// NativeExecutionFFISigningPrimitive is a minimal cryptographic primitive +// interface used by the reusable native FFI executor adapter. +type NativeExecutionFFISigningPrimitive interface { + Sign( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, + ) (*frost.Signature, error) + RegisterUnmarshallers(channel net.BroadcastChannel) +} + +type nativeExecutionFFIExecutorAdapter struct { + primitive NativeExecutionFFISigningPrimitive +} + +// NewNativeExecutionFFIExecutorAdapter wraps a native FFI signing primitive as +// a NativeExecutionFFIExecutor. +func NewNativeExecutionFFIExecutorAdapter( + primitive NativeExecutionFFISigningPrimitive, +) (NativeExecutionFFIExecutor, error) { + if primitive == nil { + return nil, fmt.Errorf("native execution FFI signing primitive is nil") + } + + return &nativeExecutionFFIExecutorAdapter{ + primitive: primitive, + }, nil +} + +// RegisterNativeExecutionFFISigningPrimitive registers a native FFI signing +// primitive by adapting it to NativeExecutionFFIExecutor. +func RegisterNativeExecutionFFISigningPrimitive( + primitive NativeExecutionFFISigningPrimitive, +) error { + executor, err := NewNativeExecutionFFIExecutorAdapter(primitive) + if err != nil { + return err + } + + return RegisterNativeExecutionFFIExecutor(executor) +} + +func (nefea *nativeExecutionFFIExecutorAdapter) Execute( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + if request == nil { + return nil, fmt.Errorf("request is nil") + } + + if request.Message == nil { + return nil, fmt.Errorf("request message is nil") + } + + signerMaterial, err := request.NativeSignerMaterial() + if err != nil { + return nil, err + } + + signature, err := nefea.primitive.Sign( + ctx, + logger, + &NativeExecutionFFISigningRequest{ + Message: request.Message, + SessionID: request.SessionID, + MemberIndex: request.MemberIndex, + GroupSize: request.GroupSize, + DishonestThreshold: request.DishonestThreshold, + SignerMaterial: signerMaterial, + Attempt: cloneAttempt(request.Attempt), + }, + ) + if err != nil { + return nil, err + } + + if signature == nil { + return nil, fmt.Errorf("native FFI signing primitive returned nil signature") + } + + return &Result{ + Signature: signature, + Attempt: cloneAttempt(request.Attempt), + }, nil +} + +func (nefea *nativeExecutionFFIExecutorAdapter) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + nefea.primitive.RegisterUnmarshallers(channel) +} diff --git a/pkg/frost/signing/native_ffi_executor_adapter_test.go b/pkg/frost/signing/native_ffi_executor_adapter_test.go new file mode 100644 index 0000000000..a671922a65 --- /dev/null +++ b/pkg/frost/signing/native_ffi_executor_adapter_test.go @@ -0,0 +1,308 @@ +package signing + +import ( + "context" + "errors" + "math/big" + "strings" + "testing" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +type mockNativeExecutionFFISigningPrimitive struct { + signCalls int + lastRequest *NativeExecutionFFISigningRequest + signature *frost.Signature + signErr error + registerCalls int + lastChannel net.BroadcastChannel +} + +func (mnefsp *mockNativeExecutionFFISigningPrimitive) Sign( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + mnefsp.signCalls++ + mnefsp.lastRequest = request + return mnefsp.signature, mnefsp.signErr +} + +func (mnefsp *mockNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + mnefsp.registerCalls++ + mnefsp.lastChannel = channel +} + +func TestNewNativeExecutionFFIExecutorAdapter_NilPrimitive(t *testing.T) { + _, err := NewNativeExecutionFFIExecutorAdapter(nil) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "native execution FFI signing primitive is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "native execution FFI signing primitive is nil", + err, + ) + } +} + +func TestNativeExecutionFFIExecutorAdapter_Execute_ValidatesRequest(t *testing.T) { + executor, err := NewNativeExecutionFFIExecutorAdapter( + &mockNativeExecutionFFISigningPrimitive{}, + ) + if err != nil { + t.Fatalf("unexpected adapter setup error: [%v]", err) + } + + _, err = executor.Execute(context.Background(), nil, nil) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "request is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "request is nil", + err, + ) + } +} + +func TestNativeExecutionFFIExecutorAdapter_Execute_ValidatesMessage(t *testing.T) { + executor, err := NewNativeExecutionFFIExecutorAdapter( + &mockNativeExecutionFFISigningPrimitive{}, + ) + if err != nil { + t.Fatalf("unexpected adapter setup error: [%v]", err) + } + + _, err = executor.Execute(context.Background(), nil, &Request{ + SignerMaterial: []byte{0x01}, + }) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "request message is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "request message is nil", + err, + ) + } +} + +func TestNativeExecutionFFIExecutorAdapter_Execute_ValidatesSignerMaterial( + t *testing.T, +) { + executor, err := NewNativeExecutionFFIExecutorAdapter( + &mockNativeExecutionFFISigningPrimitive{}, + ) + if err != nil { + t.Fatalf("unexpected adapter setup error: [%v]", err) + } + + _, err = executor.Execute(context.Background(), nil, &Request{ + Message: big.NewInt(1), + SignerMaterial: "invalid", + }) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "native signer material has wrong type") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "native signer material has wrong type", + err, + ) + } +} + +func TestNativeExecutionFFIExecutorAdapter_Execute_DelegatesToPrimitive( + t *testing.T, +) { + expectedSignature := &frost.Signature{ + R: [frost.SignatureComponentSize]byte{0x01}, + S: [frost.SignatureComponentSize]byte{0x02}, + } + + primitive := &mockNativeExecutionFFISigningPrimitive{ + signature: expectedSignature, + } + + executor, err := NewNativeExecutionFFIExecutorAdapter(primitive) + if err != nil { + t.Fatalf("unexpected adapter setup error: [%v]", err) + } + + attempt := &Attempt{ + Number: 3, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2, 3}, + ExcludedMembersIndexes: []group.MemberIndex{4}, + } + + result, err := executor.Execute(context.Background(), nil, &Request{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 2, + GroupSize: 5, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte{0xaa}, + }, + Attempt: attempt, + }) + if err != nil { + t.Fatalf("unexpected execute error: [%v]", err) + } + + if result == nil || result.Signature != expectedSignature { + t.Fatalf( + "unexpected result signature\nexpected: [%+v]\nactual: [%+v]", + expectedSignature, + result, + ) + } + + if primitive.signCalls != 1 { + t.Fatalf("unexpected primitive sign calls count: [%d]", primitive.signCalls) + } + + if primitive.lastRequest == nil { + t.Fatal("expected primitive request") + } + + if primitive.lastRequest.SignerMaterial == nil { + t.Fatal("expected signer material in primitive request") + } + + if primitive.lastRequest.Attempt == attempt { + t.Fatal("expected attempt clone in primitive request") + } +} + +func TestNativeExecutionFFIExecutorAdapter_Execute_PropagatesPrimitiveError( + t *testing.T, +) { + expectedErr := errors.New("native signer failure") + primitive := &mockNativeExecutionFFISigningPrimitive{ + signErr: expectedErr, + } + + executor, err := NewNativeExecutionFFIExecutorAdapter(primitive) + if err != nil { + t.Fatalf("unexpected adapter setup error: [%v]", err) + } + + _, err = executor.Execute(context.Background(), nil, &Request{ + Message: big.NewInt(1), + SignerMaterial: []byte{0x01}, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, expectedErr) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + expectedErr, + err, + ) + } +} + +func TestNativeExecutionFFIExecutorAdapter_Execute_RejectsNilSignature( + t *testing.T, +) { + primitive := &mockNativeExecutionFFISigningPrimitive{} + + executor, err := NewNativeExecutionFFIExecutorAdapter(primitive) + if err != nil { + t.Fatalf("unexpected adapter setup error: [%v]", err) + } + + _, err = executor.Execute(context.Background(), nil, &Request{ + Message: big.NewInt(1), + SignerMaterial: []byte{0x01}, + }) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "returned nil signature") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "returned nil signature", + err, + ) + } +} + +func TestNativeExecutionFFIExecutorAdapter_RegisterUnmarshallers_Delegates( + t *testing.T, +) { + primitive := &mockNativeExecutionFFISigningPrimitive{} + + executor, err := NewNativeExecutionFFIExecutorAdapter(primitive) + if err != nil { + t.Fatalf("unexpected adapter setup error: [%v]", err) + } + + var channel net.BroadcastChannel + executor.RegisterUnmarshallers(channel) + + if primitive.registerCalls != 1 { + t.Fatalf( + "unexpected register unmarshallers calls count: [%d]", + primitive.registerCalls, + ) + } +} + +func TestRegisterNativeExecutionFFISigningPrimitive_Nil(t *testing.T) { + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + err := RegisterNativeExecutionFFISigningPrimitive(nil) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "native execution FFI signing primitive is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "native execution FFI signing primitive is nil", + err, + ) + } +} + +func TestRegisterNativeExecutionFFISigningPrimitive_RegistersExecutor(t *testing.T) { + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + err := RegisterNativeExecutionFFISigningPrimitive( + &mockNativeExecutionFFISigningPrimitive{ + signature: &frost.Signature{}, + }, + ) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + executor := currentNativeExecutionFFIExecutor() + if executor == nil { + t.Fatal("expected native FFI executor registration") + } +} diff --git a/pkg/frost/signing/native_signer_material.go b/pkg/frost/signing/native_signer_material.go new file mode 100644 index 0000000000..af7b84e74f --- /dev/null +++ b/pkg/frost/signing/native_signer_material.go @@ -0,0 +1,90 @@ +package signing + +import "fmt" + +const ( + // NativeSignerMaterialFormatFrostUniFFIV1 is the canonical format name for + // serialized signer material expected by UniFFI-based native FROST bridges. + NativeSignerMaterialFormatFrostUniFFIV1 = "frost-uniffi-v1" +) + +// NativeSignerMaterial carries backend-native signer material required by +// native FROST execution paths. +type NativeSignerMaterial struct { + Format string + Payload []byte +} + +func (nsm *NativeSignerMaterial) clone() *NativeSignerMaterial { + if nsm == nil { + return nil + } + + result := &NativeSignerMaterial{ + Format: nsm.Format, + } + + if len(nsm.Payload) > 0 { + result.Payload = append([]byte{}, nsm.Payload...) + } + + return result +} + +func (nsm *NativeSignerMaterial) validate() error { + if nsm == nil { + return fmt.Errorf("native signer material is nil") + } + + if nsm.Format == "" { + return fmt.Errorf("native signer material format is empty") + } + + if len(nsm.Payload) == 0 { + return fmt.Errorf("native signer material payload is empty") + } + + return nil +} + +// NativeSignerMaterial resolves native signer material required by +// FFI-backed native execution. +// +// Supported Request.SignerMaterial forms: +// - *NativeSignerMaterial +// - NativeSignerMaterial +// - []byte (interpreted as NativeSignerMaterialFormatFrostUniFFIV1 payload) +func (r *Request) NativeSignerMaterial() (*NativeSignerMaterial, error) { + if r == nil { + return nil, fmt.Errorf("request is nil") + } + + if r.SignerMaterial == nil { + return nil, fmt.Errorf("native signer material is nil") + } + + var nativeSignerMaterial *NativeSignerMaterial + + switch signerMaterial := r.SignerMaterial.(type) { + case *NativeSignerMaterial: + nativeSignerMaterial = signerMaterial.clone() + case NativeSignerMaterial: + nativeSignerMaterial = signerMaterial.clone() + case []byte: + nativeSignerMaterial = &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: append([]byte{}, signerMaterial...), + } + default: + return nil, fmt.Errorf( + "native signer material has wrong type: [%T]", + r.SignerMaterial, + ) + } + + if err := nativeSignerMaterial.validate(); err != nil { + return nil, err + } + + return nativeSignerMaterial, nil +} diff --git a/pkg/frost/signing/native_signer_material_test.go b/pkg/frost/signing/native_signer_material_test.go new file mode 100644 index 0000000000..c3b92ffd08 --- /dev/null +++ b/pkg/frost/signing/native_signer_material_test.go @@ -0,0 +1,155 @@ +package signing + +import ( + "bytes" + "strings" + "testing" +) + +func TestRequest_NativeSignerMaterial_FromPointer(t *testing.T) { + input := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte{0x01, 0x02, 0x03}, + } + + request := &Request{ + SignerMaterial: input, + } + + result, err := request.NativeSignerMaterial() + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if result == input { + t.Fatal("expected a clone of native signer material") + } + + if result.Format != input.Format { + t.Fatalf( + "unexpected signer material format\nexpected: [%v]\nactual: [%v]", + input.Format, + result.Format, + ) + } + + if !bytes.Equal(result.Payload, input.Payload) { + t.Fatalf( + "unexpected signer material payload\nexpected: [%x]\nactual: [%x]", + input.Payload, + result.Payload, + ) + } +} + +func TestRequest_NativeSignerMaterial_FromValue(t *testing.T) { + request := &Request{ + SignerMaterial: NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte{0xaa, 0xbb}, + }, + } + + result, err := request.NativeSignerMaterial() + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if result.Format != NativeSignerMaterialFormatFrostUniFFIV1 { + t.Fatalf( + "unexpected signer material format\nexpected: [%v]\nactual: [%v]", + NativeSignerMaterialFormatFrostUniFFIV1, + result.Format, + ) + } +} + +func TestRequest_NativeSignerMaterial_FromBytesUsesDefaultFormat(t *testing.T) { + request := &Request{ + SignerMaterial: []byte{0x10, 0x20}, + } + + result, err := request.NativeSignerMaterial() + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if result.Format != NativeSignerMaterialFormatFrostUniFFIV1 { + t.Fatalf( + "unexpected signer material format\nexpected: [%v]\nactual: [%v]", + NativeSignerMaterialFormatFrostUniFFIV1, + result.Format, + ) + } +} + +func TestRequest_NativeSignerMaterial_NilRequest(t *testing.T) { + _, err := (*Request)(nil).NativeSignerMaterial() + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "request is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "request is nil", + err, + ) + } +} + +func TestRequest_NativeSignerMaterial_NilMaterial(t *testing.T) { + _, err := (&Request{}).NativeSignerMaterial() + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "native signer material is nil") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "native signer material is nil", + err, + ) + } +} + +func TestRequest_NativeSignerMaterial_WrongType(t *testing.T) { + request := &Request{ + SignerMaterial: "invalid", + } + + _, err := request.NativeSignerMaterial() + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "native signer material has wrong type") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "native signer material has wrong type", + err, + ) + } +} + +func TestRequest_NativeSignerMaterial_ValidationFailure(t *testing.T) { + request := &Request{ + SignerMaterial: NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte{}, + }, + } + + _, err := request.NativeSignerMaterial() + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "native signer material payload is empty") { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "native signer material payload is empty", + err, + ) + } +} From 3083e15ab438b6550717b93af5f76c818ae07862 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 18:00:52 -0600 Subject: [PATCH 24/96] frost/signing: include transport context in ffi adapter request --- .../signing/native_ffi_executor_adapter.go | 32 +++++++++++-------- ...igning_native_backend_frost_native_test.go | 19 +++++------ 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/pkg/frost/signing/native_ffi_executor_adapter.go b/pkg/frost/signing/native_ffi_executor_adapter.go index e149b563e7..8e01f616f0 100644 --- a/pkg/frost/signing/native_ffi_executor_adapter.go +++ b/pkg/frost/signing/native_ffi_executor_adapter.go @@ -14,13 +14,15 @@ import ( // NativeExecutionFFISigningRequest is the canonical request passed to a native // FFI signing primitive. type NativeExecutionFFISigningRequest struct { - Message *big.Int - SessionID string - MemberIndex group.MemberIndex - GroupSize int - DishonestThreshold int - SignerMaterial *NativeSignerMaterial - Attempt *Attempt + Message *big.Int + SessionID string + MemberIndex group.MemberIndex + GroupSize int + DishonestThreshold int + Channel net.BroadcastChannel + MembershipValidator *group.MembershipValidator + SignerMaterial *NativeSignerMaterial + Attempt *Attempt } // NativeExecutionFFISigningPrimitive is a minimal cryptographic primitive @@ -87,13 +89,15 @@ func (nefea *nativeExecutionFFIExecutorAdapter) Execute( ctx, logger, &NativeExecutionFFISigningRequest{ - Message: request.Message, - SessionID: request.SessionID, - MemberIndex: request.MemberIndex, - GroupSize: request.GroupSize, - DishonestThreshold: request.DishonestThreshold, - SignerMaterial: signerMaterial, - Attempt: cloneAttempt(request.Attempt), + Message: request.Message, + SessionID: request.SessionID, + MemberIndex: request.MemberIndex, + GroupSize: request.GroupSize, + DishonestThreshold: request.DishonestThreshold, + Channel: request.Channel, + MembershipValidator: request.MembershipValidator, + SignerMaterial: signerMaterial, + Attempt: cloneAttempt(request.Attempt), }, ) if err != nil { diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index b1507a65e0..01f96e55e0 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -10,21 +10,22 @@ import ( "testing" "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/net" ) -type noopNativeExecutionFFIExecutor struct{} +type noopNativeExecutionFFISigningPrimitive struct{} -func (nnefe *noopNativeExecutionFFIExecutor) Execute( +func (nnefsp *noopNativeExecutionFFISigningPrimitive) Sign( ctx context.Context, logger log.StandardLogger, - request *frostsigning.Request, -) (*frostsigning.Result, error) { - return nil, nil + request *frostsigning.NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + return &frost.Signature{}, nil } -func (nnefe *noopNativeExecutionFFIExecutor) RegisterUnmarshallers( +func (nnefsp *noopNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( channel net.BroadcastChannel, ) { } @@ -35,11 +36,11 @@ func TestConfigureFrostSigningBackend_FFIStrictConfigured_BuildAdapter(t *testin frostsigning.UnregisterNativeExecutionBridge() frostsigning.UnregisterNativeExecutionFFIExecutor() frostsigning.RegisterNativeExecutionAdapterForBuild() - err := frostsigning.RegisterNativeExecutionFFIExecutor( - &noopNativeExecutionFFIExecutor{}, + err := frostsigning.RegisterNativeExecutionFFISigningPrimitive( + &noopNativeExecutionFFISigningPrimitive{}, ) if err != nil { - t.Fatalf("unexpected native FFI executor registration error: [%v]", err) + t.Fatalf("unexpected native FFI primitive registration error: [%v]", err) } t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) From 31026eb3fded5cacd9a6a1dfee44389626649816 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 18:05:44 -0600 Subject: [PATCH 25/96] tbtc: persist native signer material envelope in signer state --- pkg/tbtc/marshaling.go | 21 +- pkg/tbtc/signer_material_encoding.go | 206 ++++++++++++++++++ pkg/tbtc/signer_material_encoding_test.go | 249 ++++++++++++++++++++++ 3 files changed, 468 insertions(+), 8 deletions(-) create mode 100644 pkg/tbtc/signer_material_encoding.go create mode 100644 pkg/tbtc/signer_material_encoding_test.go diff --git a/pkg/tbtc/marshaling.go b/pkg/tbtc/marshaling.go index f9cf56859c..babbfb34f7 100644 --- a/pkg/tbtc/marshaling.go +++ b/pkg/tbtc/marshaling.go @@ -43,15 +43,18 @@ func (s *signer) Marshal() ([]byte, error) { SigningGroupOperators: walletSigningGroupOperators, } - privateKeyShare, err := s.privateKeyShare.Marshal() + signerMaterialBytes, err := marshalSignerMaterialForPersistence( + s.signerMaterial, + s.privateKeyShare, + ) if err != nil { - return nil, fmt.Errorf("cannot marshal private key share: [%w]", err) + return nil, fmt.Errorf("cannot marshal signer material: [%w]", err) } return proto.Marshal(&pb.Signer{ Wallet: pbWallet, SigningGroupMemberIndex: uint32(s.signingGroupMemberIndex), - PrivateKeyShare: privateKeyShare, + PrivateKeyShare: signerMaterialBytes, }) } @@ -73,9 +76,11 @@ func (s *signer) Unmarshal(bytes []byte) error { chain.Address(pbSigner.Wallet.SigningGroupOperators[i]) } - privateKeyShare := &tecdsa.PrivateKeyShare{} - if err := privateKeyShare.Unmarshal(pbSigner.PrivateKeyShare); err != nil { - return fmt.Errorf("cannot unmarshal private key share: [%w]", err) + signerMaterial, err := unmarshalSignerMaterialFromPersistence( + pbSigner.PrivateKeyShare, + ) + if err != nil { + return fmt.Errorf("cannot unmarshal signer material: [%w]", err) } s.wallet = wallet{ @@ -83,8 +88,8 @@ func (s *signer) Unmarshal(bytes []byte) error { signingGroupOperators: walletSigningGroupOperators, } s.signingGroupMemberIndex = group.MemberIndex(pbSigner.SigningGroupMemberIndex) - s.privateKeyShare = privateKeyShare - s.signerMaterial = privateKeyShare + s.privateKeyShare = signerMaterial.privateKeyShare + s.signerMaterial = signerMaterial.signerMaterial return nil } diff --git a/pkg/tbtc/signer_material_encoding.go b/pkg/tbtc/signer_material_encoding.go new file mode 100644 index 0000000000..4665d95a22 --- /dev/null +++ b/pkg/tbtc/signer_material_encoding.go @@ -0,0 +1,206 @@ +package tbtc + +import ( + "bytes" + "encoding/binary" + "fmt" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +var signerMaterialEnvelopePrefix = []byte("tbtc-signer-material-v1:") + +type unmarshaledSignerMaterial struct { + signerMaterial any + privateKeyShare *tecdsa.PrivateKeyShare +} + +func marshalSignerMaterialForPersistence( + signerMaterial any, + fallbackPrivateKeyShare *tecdsa.PrivateKeyShare, +) ([]byte, error) { + if signerMaterial == nil { + signerMaterial = fallbackPrivateKeyShare + } + + switch material := signerMaterial.(type) { + case *tecdsa.PrivateKeyShare: + if material == nil { + return nil, fmt.Errorf("legacy private key share is nil") + } + + return material.Marshal() + case tecdsa.PrivateKeyShare: + materialCopy := material + return (&materialCopy).Marshal() + case *frostsigning.NativeSignerMaterial: + if material == nil { + return nil, fmt.Errorf("native signer material is nil") + } + + return encodeNativeSignerMaterialForPersistence( + material.Format, + material.Payload, + ) + case frostsigning.NativeSignerMaterial: + return encodeNativeSignerMaterialForPersistence( + material.Format, + material.Payload, + ) + case []byte: + return encodeNativeSignerMaterialForPersistence( + frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + material, + ) + default: + return nil, fmt.Errorf("unsupported signer material type: [%T]", signerMaterial) + } +} + +func unmarshalSignerMaterialFromPersistence( + data []byte, +) (*unmarshaledSignerMaterial, error) { + nativeSignerMaterial, isNative, err := decodeNativeSignerMaterialFromPersistence( + data, + ) + if err != nil { + return nil, err + } + + if isNative { + return &unmarshaledSignerMaterial{ + signerMaterial: nativeSignerMaterial, + privateKeyShare: nil, + }, nil + } + + privateKeyShare := &tecdsa.PrivateKeyShare{} + if err := privateKeyShare.Unmarshal(data); err != nil { + return nil, fmt.Errorf("cannot unmarshal private key share: [%w]", err) + } + + return &unmarshaledSignerMaterial{ + signerMaterial: privateKeyShare, + privateKeyShare: privateKeyShare, + }, nil +} + +func encodeNativeSignerMaterialForPersistence( + format string, + payload []byte, +) ([]byte, error) { + material := &frostsigning.NativeSignerMaterial{ + Format: format, + Payload: append([]byte{}, payload...), + } + + if err := validateNativeSignerMaterialForPersistence(material); err != nil { + return nil, err + } + + result := make([]byte, 0, len(signerMaterialEnvelopePrefix)+len(format)+len(payload)+20) + result = append(result, signerMaterialEnvelopePrefix...) + + var varintBuffer [binary.MaxVarintLen64]byte + + formatLength := binary.PutUvarint(varintBuffer[:], uint64(len(material.Format))) + result = append(result, varintBuffer[:formatLength]...) + result = append(result, []byte(material.Format)...) + + payloadLength := binary.PutUvarint(varintBuffer[:], uint64(len(material.Payload))) + result = append(result, varintBuffer[:payloadLength]...) + result = append(result, material.Payload...) + + return result, nil +} + +func decodeNativeSignerMaterialFromPersistence( + data []byte, +) ( + *frostsigning.NativeSignerMaterial, + bool, + error, +) { + if !bytes.HasPrefix(data, signerMaterialEnvelopePrefix) { + return nil, false, nil + } + + offset := len(signerMaterialEnvelopePrefix) + + formatLength, lengthBytes, err := readPersistenceUvarint(data, offset) + if err != nil { + return nil, true, fmt.Errorf("invalid signer material format length: [%w]", err) + } + offset += lengthBytes + + if offset+int(formatLength) > len(data) { + return nil, true, fmt.Errorf("signer material format length exceeds payload") + } + + format := string(data[offset : offset+int(formatLength)]) + offset += int(formatLength) + + payloadLength, lengthBytes, err := readPersistenceUvarint(data, offset) + if err != nil { + return nil, true, fmt.Errorf("invalid signer material payload length: [%w]", err) + } + offset += lengthBytes + + if offset+int(payloadLength) > len(data) { + return nil, true, fmt.Errorf("signer material payload length exceeds payload") + } + + payload := append([]byte{}, data[offset:offset+int(payloadLength)]...) + offset += int(payloadLength) + + if offset != len(data) { + return nil, true, fmt.Errorf("unexpected trailing signer material payload bytes") + } + + material := &frostsigning.NativeSignerMaterial{ + Format: format, + Payload: payload, + } + + if err := validateNativeSignerMaterialForPersistence(material); err != nil { + return nil, true, err + } + + return material, true, nil +} + +func validateNativeSignerMaterialForPersistence( + material *frostsigning.NativeSignerMaterial, +) error { + if material == nil { + return fmt.Errorf("native signer material is nil") + } + + if material.Format == "" { + return fmt.Errorf("native signer material format is empty") + } + + if len(material.Payload) == 0 { + return fmt.Errorf("native signer material payload is empty") + } + + return nil +} + +func readPersistenceUvarint(data []byte, offset int) (uint64, int, error) { + if offset >= len(data) { + return 0, 0, fmt.Errorf("offset [%d] out of bounds", offset) + } + + value, lengthBytes := binary.Uvarint(data[offset:]) + if lengthBytes == 0 { + return 0, 0, fmt.Errorf("incomplete uvarint") + } + + if lengthBytes < 0 { + return 0, 0, fmt.Errorf("overflowed uvarint") + } + + return value, lengthBytes, nil +} diff --git a/pkg/tbtc/signer_material_encoding_test.go b/pkg/tbtc/signer_material_encoding_test.go new file mode 100644 index 0000000000..1051c4e666 --- /dev/null +++ b/pkg/tbtc/signer_material_encoding_test.go @@ -0,0 +1,249 @@ +package tbtc + +import ( + "bytes" + "reflect" + "strings" + "testing" + + "github.com/google/gofuzz" + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/internal/pbutils" + "github.com/keep-network/keep-core/pkg/tbtc/gen/pb" + "github.com/keep-network/keep-core/pkg/tecdsa" + "google.golang.org/protobuf/proto" +) + +func TestMarshalSignerMaterialForPersistence_LegacyPrivateKeyShare(t *testing.T) { + signer := createMockSigner(t) + + encoded, err := marshalSignerMaterialForPersistence( + signer.privateKeyShare, + nil, + ) + if err != nil { + t.Fatalf("unexpected marshal error: [%v]", err) + } + + _, isNative, err := decodeNativeSignerMaterialFromPersistence(encoded) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + if isNative { + t.Fatal("expected legacy private key share encoding") + } + + decoded := &tecdsa.PrivateKeyShare{} + if err := decoded.Unmarshal(encoded); err != nil { + t.Fatalf("unexpected legacy unmarshal error: [%v]", err) + } +} + +func TestMarshalSignerMaterialForPersistence_NativeSignerMaterial(t *testing.T) { + payload := []byte{0xaa, 0xbb, 0xcc} + encoded, err := marshalSignerMaterialForPersistence( + &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + Payload: payload, + }, + nil, + ) + if err != nil { + t.Fatalf("unexpected marshal error: [%v]", err) + } + + decoded, isNative, err := decodeNativeSignerMaterialFromPersistence(encoded) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + if !isNative { + t.Fatal("expected native signer material envelope") + } + + if decoded == nil { + t.Fatal("expected native signer material") + } + + if decoded.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { + t.Fatalf( + "unexpected decoded format\nexpected: [%v]\nactual: [%v]", + frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + decoded.Format, + ) + } + + if !bytes.Equal(decoded.Payload, payload) { + t.Fatalf( + "unexpected decoded payload\nexpected: [%x]\nactual: [%x]", + payload, + decoded.Payload, + ) + } +} + +func TestUnmarshalSignerMaterialFromPersistence_NativeEnvelope(t *testing.T) { + encoded, err := encodeNativeSignerMaterialForPersistence( + frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + []byte{0x10, 0x20}, + ) + if err != nil { + t.Fatalf("unexpected encode error: [%v]", err) + } + + decoded, err := unmarshalSignerMaterialFromPersistence(encoded) + if err != nil { + t.Fatalf("unexpected unmarshal error: [%v]", err) + } + + if decoded.privateKeyShare != nil { + t.Fatal("expected nil private key share for native signer material") + } + + nativeSignerMaterial, ok := decoded.signerMaterial.(*frostsigning.NativeSignerMaterial) + if !ok { + t.Fatalf( + "unexpected signer material type\nexpected: [%T]\nactual: [%T]", + &frostsigning.NativeSignerMaterial{}, + decoded.signerMaterial, + ) + } + + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { + t.Fatalf( + "unexpected signer material format\nexpected: [%v]\nactual: [%v]", + frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + nativeSignerMaterial.Format, + ) + } +} + +func TestUnmarshalSignerMaterialFromPersistence_CorruptedNativeEnvelope(t *testing.T) { + encoded, err := encodeNativeSignerMaterialForPersistence( + frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + []byte{0x10, 0x20}, + ) + if err != nil { + t.Fatalf("unexpected encode error: [%v]", err) + } + + encoded = encoded[:len(encoded)-1] + + _, err = unmarshalSignerMaterialFromPersistence(encoded) + if err == nil { + t.Fatal("expected unmarshal error") + } + + if !strings.Contains(err.Error(), "signer material payload length exceeds payload") { + t.Fatalf( + "unexpected unmarshal error\nexpected substring: [%s]\nactual: [%v]", + "signer material payload length exceeds payload", + err, + ) + } +} + +func TestMarshalSignerMaterialForPersistence_UnsupportedType(t *testing.T) { + _, err := marshalSignerMaterialForPersistence(struct{}{}, nil) + if err == nil { + t.Fatal("expected marshal error") + } + + if !strings.Contains(err.Error(), "unsupported signer material type") { + t.Fatalf( + "unexpected marshal error\nexpected substring: [%s]\nactual: [%v]", + "unsupported signer material type", + err, + ) + } +} + +func TestSignerMarshalling_NativeSignerMaterialRoundtrip(t *testing.T) { + legacySigner := createMockSigner(t) + marshaled := &signer{ + wallet: legacySigner.wallet, + signingGroupMemberIndex: legacySigner.signingGroupMemberIndex, + signerMaterial: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte{0x44, 0x55, 0x66}, + }, + } + unmarshaled := &signer{} + + if err := pbutils.RoundTrip(marshaled, unmarshaled); err != nil { + t.Fatal(err) + } + + if unmarshaled.privateKeyShare != nil { + t.Fatal("expected nil private key share for native signer material") + } + + if !reflect.DeepEqual(marshaled.wallet, unmarshaled.wallet) { + t.Fatalf( + "unexpected wallet state after roundtrip\nexpected: [%+v]\nactual: [%+v]", + marshaled.wallet, + unmarshaled.wallet, + ) + } + + if marshaled.signingGroupMemberIndex != unmarshaled.signingGroupMemberIndex { + t.Fatalf( + "unexpected signer member index\nexpected: [%v]\nactual: [%v]", + marshaled.signingGroupMemberIndex, + unmarshaled.signingGroupMemberIndex, + ) + } + + nativeSignerMaterial, ok := unmarshaled.signerMaterial.(*frostsigning.NativeSignerMaterial) + if !ok { + t.Fatalf( + "unexpected signer material type\nexpected: [%T]\nactual: [%T]", + &frostsigning.NativeSignerMaterial{}, + unmarshaled.signerMaterial, + ) + } + + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { + t.Fatalf( + "unexpected signer material format\nexpected: [%v]\nactual: [%v]", + frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + nativeSignerMaterial.Format, + ) + } + + if !bytes.Equal(nativeSignerMaterial.Payload, []byte{0x44, 0x55, 0x66}) { + t.Fatalf( + "unexpected signer material payload\nexpected: [%x]\nactual: [%x]", + []byte{0x44, 0x55, 0x66}, + nativeSignerMaterial.Payload, + ) + } +} + +func TestSignerMarshalling_LegacyEncodingDoesNotUseNativeEnvelope(t *testing.T) { + signer := createMockSigner(t) + + encodedSigner, err := signer.Marshal() + if err != nil { + t.Fatalf("unexpected marshal error: [%v]", err) + } + + pbSigner := &pb.Signer{} + if err := proto.Unmarshal(encodedSigner, pbSigner); err != nil { + t.Fatalf("unexpected proto unmarshal error: [%v]", err) + } + + if bytes.HasPrefix(pbSigner.PrivateKeyShare, signerMaterialEnvelopePrefix) { + t.Fatal("expected legacy signer encoding without native envelope") + } +} + +func TestFuzzDecodeNativeSignerMaterialFromPersistence(t *testing.T) { + for i := 0; i < 10; i++ { + var data []byte + fuzz.New().NilChance(0.1).NumElements(0, 256).Fuzz(&data) + + _, _, _ = decodeNativeSignerMaterialFromPersistence(data) + } +} From a1525a023d6916d48e9286e74f0ae48976bfb9bd Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 18:09:03 -0600 Subject: [PATCH 26/96] frost/native: fallback when ffi signer material is unavailable --- .../signing/native_ffi_executor_adapter.go | 2 +- .../native_ffi_executor_adapter_test.go | 8 ++ ...igning_native_backend_frost_native_test.go | 86 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/pkg/frost/signing/native_ffi_executor_adapter.go b/pkg/frost/signing/native_ffi_executor_adapter.go index 8e01f616f0..f5539f5dae 100644 --- a/pkg/frost/signing/native_ffi_executor_adapter.go +++ b/pkg/frost/signing/native_ffi_executor_adapter.go @@ -82,7 +82,7 @@ func (nefea *nativeExecutionFFIExecutorAdapter) Execute( signerMaterial, err := request.NativeSignerMaterial() if err != nil { - return nil, err + return nil, fmt.Errorf("%w: [%v]", ErrNativeCryptographyUnavailable, err) } signature, err := nefea.primitive.Sign( diff --git a/pkg/frost/signing/native_ffi_executor_adapter_test.go b/pkg/frost/signing/native_ffi_executor_adapter_test.go index a671922a65..565e5eaaf5 100644 --- a/pkg/frost/signing/native_ffi_executor_adapter_test.go +++ b/pkg/frost/signing/native_ffi_executor_adapter_test.go @@ -118,6 +118,14 @@ func TestNativeExecutionFFIExecutorAdapter_Execute_ValidatesSignerMaterial( t.Fatal("expected error") } + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + if !strings.Contains(err.Error(), "native signer material has wrong type") { t.Fatalf( "unexpected error\nexpected substring: [%s]\nactual: [%v]", diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index 01f96e55e0..1d67eea981 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -30,6 +30,24 @@ func (nnefsp *noopNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( ) { } +type countingNativeExecutionFFISigningPrimitive struct { + signCalls int +} + +func (cnefsp *countingNativeExecutionFFISigningPrimitive) Sign( + ctx context.Context, + logger log.StandardLogger, + request *frostsigning.NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + cnefsp.signCalls++ + return &frost.Signature{}, nil +} + +func (cnefsp *countingNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { +} + func TestConfigureFrostSigningBackend_FFIStrictConfigured_BuildAdapter(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() @@ -153,3 +171,71 @@ func TestSigningExecutor_Sign_NativeBackend(t *testing.T) { t.Fatal("wrong end block") } } + +func TestSigningExecutor_Sign_NativeBackend_FallsBackWhenOnlyLegacySignerMaterial( + t *testing.T, +) { + executor := setupSigningExecutor(t) + + primitive := &countingNativeExecutionFFISigningPrimitive{} + + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.UnregisterNativeExecutionBridge() + frostsigning.UnregisterNativeExecutionFFIExecutor() + frostsigning.RegisterNativeExecutionAdapterForBuild() + err := frostsigning.RegisterNativeExecutionFFISigningPrimitive(primitive) + if err != nil { + t.Fatalf("unexpected native FFI primitive registration error: [%v]", err) + } + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) + + err = configureFrostSigningBackend(Config{FrostSigningBackend: "native"}) + if err != nil { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + message := big.NewInt(100) + startBlock := uint64(0) + + signature, _, endBlock, err := executor.sign(ctx, message, startBlock) + if err != nil { + t.Fatalf("unexpected native backend signing error: [%v]", err) + } + + if primitive.signCalls != 0 { + t.Fatalf( + "unexpected native primitive sign calls count\nexpected: [%d]\nactual: [%d]", + 0, + primitive.signCalls, + ) + } + + walletPublicKey := executor.wallet().publicKey + if !ecdsa.Verify( + walletPublicKey, + message.Bytes(), + new(big.Int).SetBytes(signature.R[:]), + new(big.Int).SetBytes(signature.S[:]), + ) { + t.Fatalf("invalid signature: [%+v]", signature) + } + + if endBlock <= startBlock { + t.Fatal("wrong end block") + } +} From eeaad8fea213f3fa5d57bc5e9b562efdf8068dce Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 18:13:59 -0600 Subject: [PATCH 27/96] tbtc: add signer-material resolver hook for dkg signer creation --- pkg/frost/signing/backend.go | 1 + .../native_ffi_primitive_registration.go | 10 ++ ...tive_ffi_primitive_registration_default.go | 5 + ...ffi_primitive_registration_frost_native.go | 5 + pkg/tbtc/dkg.go | 6 + pkg/tbtc/signer_material_resolver.go | 71 ++++++++++ pkg/tbtc/signer_material_resolver_test.go | 122 ++++++++++++++++++ pkg/tbtc/wallet.go | 7 +- 8 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 pkg/frost/signing/native_ffi_primitive_registration.go create mode 100644 pkg/frost/signing/native_ffi_primitive_registration_default.go create mode 100644 pkg/frost/signing/native_ffi_primitive_registration_frost_native.go create mode 100644 pkg/tbtc/signer_material_resolver.go create mode 100644 pkg/tbtc/signer_material_resolver_test.go diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index 48ff3489b2..dce90bd536 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -199,6 +199,7 @@ func UnregisterNativeExecutionAdapter() { // On `frost_native` builds, this registers the tagged native adapter. func RegisterNativeExecutionAdapterForBuild() { registerNativeExecutionAdapterForBuild() + RegisterNativeExecutionFFISigningPrimitiveForBuild() } func currentNativeExecutionBackend() (ExecutionBackend, error) { diff --git a/pkg/frost/signing/native_ffi_primitive_registration.go b/pkg/frost/signing/native_ffi_primitive_registration.go new file mode 100644 index 0000000000..9901676f2b --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration.go @@ -0,0 +1,10 @@ +package signing + +// RegisterNativeExecutionFFISigningPrimitiveForBuild attempts to register +// build-flavor native FFI signing primitive bindings. +// +// On default builds, this is a no-op. +// On `frost_native` builds, this can be wired to a concrete primitive. +func RegisterNativeExecutionFFISigningPrimitiveForBuild() { + registerNativeExecutionFFISigningPrimitiveForBuild() +} diff --git a/pkg/frost/signing/native_ffi_primitive_registration_default.go b/pkg/frost/signing/native_ffi_primitive_registration_default.go new file mode 100644 index 0000000000..6cb07834e8 --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_default.go @@ -0,0 +1,5 @@ +//go:build !frost_native + +package signing + +func registerNativeExecutionFFISigningPrimitiveForBuild() {} diff --git a/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go b/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go new file mode 100644 index 0000000000..ef7ba5c5dc --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go @@ -0,0 +1,5 @@ +//go:build frost_native + +package signing + +func registerNativeExecutionFFISigningPrimitiveForBuild() {} diff --git a/pkg/tbtc/dkg.go b/pkg/tbtc/dkg.go index 177e225a18..56c08291ee 100644 --- a/pkg/tbtc/dkg.go +++ b/pkg/tbtc/dkg.go @@ -521,11 +521,17 @@ func (de *dkgExecutor) registerSigner( ) } + signerMaterial, err := resolveSignerMaterial(result.PrivateKeyShare) + if err != nil { + return nil, fmt.Errorf("failed to resolve signer material: [%w]", err) + } + signer := newSigner( result.PrivateKeyShare.PublicKey(), finalSigningGroupOperators, finalSigningGroupMemberIndex, result.PrivateKeyShare, + signerMaterial, ) err = de.walletRegistry.registerSigner(signer) diff --git a/pkg/tbtc/signer_material_resolver.go b/pkg/tbtc/signer_material_resolver.go new file mode 100644 index 0000000000..246b5b6929 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver.go @@ -0,0 +1,71 @@ +package tbtc + +import ( + "fmt" + "sync" + + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +// SignerMaterialResolver derives signer material from a legacy private key +// share. Implementations can provide backend-native signer material while +// preserving fallback compatibility. +type SignerMaterialResolver interface { + ResolveSignerMaterial(privateKeyShare *tecdsa.PrivateKeyShare) (any, error) +} + +type legacyPrivateKeyShareSignerMaterialResolver struct{} + +func (lpkssmr *legacyPrivateKeyShareSignerMaterialResolver) ResolveSignerMaterial( + privateKeyShare *tecdsa.PrivateKeyShare, +) (any, error) { + if privateKeyShare == nil { + return nil, fmt.Errorf("private key share is nil") + } + + return privateKeyShare, nil +} + +var ( + signerMaterialResolverMutex sync.RWMutex + signerMaterialResolver SignerMaterialResolver = &legacyPrivateKeyShareSignerMaterialResolver{} +) + +// RegisterSignerMaterialResolver registers a signer material resolver used by +// DKG signer construction. +func RegisterSignerMaterialResolver(resolver SignerMaterialResolver) error { + if resolver == nil { + return fmt.Errorf("signer material resolver is nil") + } + + signerMaterialResolverMutex.Lock() + defer signerMaterialResolverMutex.Unlock() + + signerMaterialResolver = resolver + + return nil +} + +// UnregisterSignerMaterialResolver restores the default legacy resolver. +func UnregisterSignerMaterialResolver() { + signerMaterialResolverMutex.Lock() + defer signerMaterialResolverMutex.Unlock() + + signerMaterialResolver = &legacyPrivateKeyShareSignerMaterialResolver{} +} + +func currentSignerMaterialResolver() SignerMaterialResolver { + signerMaterialResolverMutex.RLock() + defer signerMaterialResolverMutex.RUnlock() + + return signerMaterialResolver +} + +func resolveSignerMaterial(privateKeyShare *tecdsa.PrivateKeyShare) (any, error) { + resolver := currentSignerMaterialResolver() + if resolver == nil { + return nil, fmt.Errorf("signer material resolver is nil") + } + + return resolver.ResolveSignerMaterial(privateKeyShare) +} diff --git a/pkg/tbtc/signer_material_resolver_test.go b/pkg/tbtc/signer_material_resolver_test.go new file mode 100644 index 0000000000..2a167e9ca0 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_test.go @@ -0,0 +1,122 @@ +package tbtc + +import ( + "errors" + "testing" + + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +type staticSignerMaterialResolver struct { + result any + err error +} + +func (ssmr *staticSignerMaterialResolver) ResolveSignerMaterial( + privateKeyShare *tecdsa.PrivateKeyShare, +) (any, error) { + return ssmr.result, ssmr.err +} + +func TestRegisterSignerMaterialResolver_Nil(t *testing.T) { + err := RegisterSignerMaterialResolver(nil) + if err == nil { + t.Fatal("expected error") + } +} + +func TestResolveSignerMaterial_DefaultResolver(t *testing.T) { + UnregisterSignerMaterialResolver() + t.Cleanup(UnregisterSignerMaterialResolver) + + privateKeyShare := createMockSigner(t).privateKeyShare + + result, err := resolveSignerMaterial(privateKeyShare) + if err != nil { + t.Fatalf("unexpected resolver error: [%v]", err) + } + + resolvedPrivateKeyShare, ok := result.(*tecdsa.PrivateKeyShare) + if !ok { + t.Fatalf( + "unexpected resolved signer material type\nexpected: [%T]\nactual: [%T]", + &tecdsa.PrivateKeyShare{}, + result, + ) + } + + if resolvedPrivateKeyShare != privateKeyShare { + t.Fatalf( + "unexpected resolved private key share\nexpected: [%v]\nactual: [%v]", + privateKeyShare, + resolvedPrivateKeyShare, + ) + } +} + +func TestResolveSignerMaterial_RegisteredResolver(t *testing.T) { + UnregisterSignerMaterialResolver() + t.Cleanup(UnregisterSignerMaterialResolver) + + expected := []byte{0xaa, 0xbb} + err := RegisterSignerMaterialResolver( + &staticSignerMaterialResolver{ + result: expected, + }, + ) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + result, err := resolveSignerMaterial(createMockSigner(t).privateKeyShare) + if err != nil { + t.Fatalf("unexpected resolver error: [%v]", err) + } + + resultBytes, ok := result.([]byte) + if !ok { + t.Fatalf( + "unexpected resolved signer material type\nexpected: [%T]\nactual: [%T]", + []byte{}, + result, + ) + } + + if len(resultBytes) != len(expected) || + resultBytes[0] != expected[0] || + resultBytes[1] != expected[1] { + t.Fatalf( + "unexpected resolved signer material\nexpected: [%x]\nactual: [%x]", + expected, + resultBytes, + ) + } +} + +func TestResolveSignerMaterial_ResolverError(t *testing.T) { + UnregisterSignerMaterialResolver() + t.Cleanup(UnregisterSignerMaterialResolver) + + expectedErr := errors.New("resolver error") + err := RegisterSignerMaterialResolver( + &staticSignerMaterialResolver{ + err: expectedErr, + }, + ) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + _, err = resolveSignerMaterial(createMockSigner(t).privateKeyShare) + if err == nil { + t.Fatal("expected resolver error") + } + + if !errors.Is(err, expectedErr) { + t.Fatalf( + "unexpected resolver error\nexpected: [%v]\nactual: [%v]", + expectedErr, + err, + ) + } +} diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index ac19b2baa6..dbb1543f09 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -792,17 +792,22 @@ func newSigner( walletSigningGroupOperators []chain.Address, signingGroupMemberIndex group.MemberIndex, privateKeyShare *tecdsa.PrivateKeyShare, + signerMaterial any, ) *signer { wallet := wallet{ publicKey: walletPublicKey, signingGroupOperators: walletSigningGroupOperators, } + if signerMaterial == nil { + signerMaterial = privateKeyShare + } + return &signer{ wallet: wallet, signingGroupMemberIndex: signingGroupMemberIndex, privateKeyShare: privateKeyShare, - signerMaterial: privateKeyShare, + signerMaterial: signerMaterial, } } From 4069ffe16f8a6647a08f7d5dce47c9a70e68b121 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 18:20:04 -0600 Subject: [PATCH 28/96] frost/native: add provider-based build registration hooks --- pkg/frost/signing/backend.go | 13 +- .../native_ffi_primitive_registration.go | 51 +++++++- ...tive_ffi_primitive_registration_default.go | 4 +- ...ffi_primitive_registration_frost_native.go | 20 +++- ...rimitive_registration_frost_native_test.go | 79 +++++++++++++ .../native_ffi_primitive_registration_test.go | 41 +++++++ pkg/tbtc/node.go | 7 ++ pkg/tbtc/signer_material_resolver.go | 42 ++++++- pkg/tbtc/signer_material_resolver_build.go | 7 ++ .../signer_material_resolver_build_default.go | 7 ++ ...er_material_resolver_build_frost_native.go | 23 ++++ ...terial_resolver_build_frost_native_test.go | 111 ++++++++++++++++++ pkg/tbtc/signer_material_resolver_test.go | 42 +++++++ 13 files changed, 436 insertions(+), 11 deletions(-) create mode 100644 pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go create mode 100644 pkg/frost/signing/native_ffi_primitive_registration_test.go create mode 100644 pkg/tbtc/signer_material_resolver_build.go create mode 100644 pkg/tbtc/signer_material_resolver_build_default.go create mode 100644 pkg/tbtc/signer_material_resolver_build_frost_native.go create mode 100644 pkg/tbtc/signer_material_resolver_build_frost_native_test.go diff --git a/pkg/frost/signing/backend.go b/pkg/frost/signing/backend.go index dce90bd536..4bf01e76a3 100644 --- a/pkg/frost/signing/backend.go +++ b/pkg/frost/signing/backend.go @@ -38,12 +38,13 @@ var ( // nativeExecutionFFIExecutor are process-global runtime state. Tests // mutating this state must run sequentially; do not use t.Parallel in such // tests. - executionBackendMutex sync.RWMutex - executionBackend ExecutionBackend = newLegacyExecutionBackend() - nativeExecutionAdapter NativeExecutionAdapter - registeredNativeExecBridge NativeExecutionBridge - nativeExecutionFFIExecutor NativeExecutionFFIExecutor - nativeExecutionMode = nativeExecutionModeFallbackAllowed + executionBackendMutex sync.RWMutex + executionBackend ExecutionBackend = newLegacyExecutionBackend() + nativeExecutionAdapter NativeExecutionAdapter + registeredNativeExecBridge NativeExecutionBridge + nativeExecutionFFIExecutor NativeExecutionFFIExecutor + nativeExecutionFFISigningPrimitiveProviderForBuild NativeExecutionFFISigningPrimitiveProviderForBuild + nativeExecutionMode = nativeExecutionModeFallbackAllowed ) // LegacyExecutionBackendName is a stable identifier of the transitional diff --git a/pkg/frost/signing/native_ffi_primitive_registration.go b/pkg/frost/signing/native_ffi_primitive_registration.go index 9901676f2b..18fc204600 100644 --- a/pkg/frost/signing/native_ffi_primitive_registration.go +++ b/pkg/frost/signing/native_ffi_primitive_registration.go @@ -1,10 +1,59 @@ package signing +import "fmt" + +// NativeExecutionFFISigningPrimitiveProviderForBuild produces a native FFI +// signing primitive for the current build/runtime flavor. +type NativeExecutionFFISigningPrimitiveProviderForBuild func() ( + NativeExecutionFFISigningPrimitive, + error, +) + +// RegisterNativeExecutionFFISigningPrimitiveProviderForBuild registers +// build-scoped primitive provider used by +// RegisterNativeExecutionFFISigningPrimitiveForBuild. +func RegisterNativeExecutionFFISigningPrimitiveProviderForBuild( + provider NativeExecutionFFISigningPrimitiveProviderForBuild, +) error { + if provider == nil { + return fmt.Errorf("native execution FFI signing primitive provider is nil") + } + + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeExecutionFFISigningPrimitiveProviderForBuild = provider + + return nil +} + +// UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild clears +// build-scoped primitive provider registration. +func UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeExecutionFFISigningPrimitiveProviderForBuild = nil +} + +func currentNativeExecutionFFISigningPrimitiveProviderForBuild() NativeExecutionFFISigningPrimitiveProviderForBuild { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return nativeExecutionFFISigningPrimitiveProviderForBuild +} + // RegisterNativeExecutionFFISigningPrimitiveForBuild attempts to register // build-flavor native FFI signing primitive bindings. // // On default builds, this is a no-op. // On `frost_native` builds, this can be wired to a concrete primitive. func RegisterNativeExecutionFFISigningPrimitiveForBuild() { - registerNativeExecutionFFISigningPrimitiveForBuild() + err := registerNativeExecutionFFISigningPrimitiveForBuild() + if err != nil { + panic(fmt.Sprintf( + "failed to register build-tagged native FFI signing primitive: [%v]", + err, + )) + } } diff --git a/pkg/frost/signing/native_ffi_primitive_registration_default.go b/pkg/frost/signing/native_ffi_primitive_registration_default.go index 6cb07834e8..a68007ea45 100644 --- a/pkg/frost/signing/native_ffi_primitive_registration_default.go +++ b/pkg/frost/signing/native_ffi_primitive_registration_default.go @@ -2,4 +2,6 @@ package signing -func registerNativeExecutionFFISigningPrimitiveForBuild() {} +func registerNativeExecutionFFISigningPrimitiveForBuild() error { + return nil +} diff --git a/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go b/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go index ef7ba5c5dc..b029d3bf6f 100644 --- a/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go @@ -2,4 +2,22 @@ package signing -func registerNativeExecutionFFISigningPrimitiveForBuild() {} +import "fmt" + +func registerNativeExecutionFFISigningPrimitiveForBuild() error { + provider := currentNativeExecutionFFISigningPrimitiveProviderForBuild() + if provider == nil { + return nil + } + + primitive, err := provider() + if err != nil { + return err + } + + if primitive == nil { + return fmt.Errorf("native execution FFI signing primitive is nil") + } + + return RegisterNativeExecutionFFISigningPrimitive(primitive) +} diff --git a/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go new file mode 100644 index 0000000000..af39c064cc --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go @@ -0,0 +1,79 @@ +//go:build frost_native + +package signing + +import ( + "errors" + "strings" + "testing" + + "github.com/keep-network/keep-core/pkg/frost" +) + +func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_UsesProvider( + t *testing.T, +) { + UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + err := RegisterNativeExecutionFFISigningPrimitiveProviderForBuild( + func() (NativeExecutionFFISigningPrimitive, error) { + return &mockNativeExecutionFFISigningPrimitive{ + signature: &frost.Signature{}, + }, nil + }, + ) + if err != nil { + t.Fatalf("unexpected provider registration error: [%v]", err) + } + + RegisterNativeExecutionFFISigningPrimitiveForBuild() + + if currentNativeExecutionFFIExecutor() == nil { + t.Fatal("expected FFI executor registration from build provider") + } +} + +func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_ProviderErrorPanics( + t *testing.T, +) { + UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + expectedErr := errors.New("provider error") + + err := RegisterNativeExecutionFFISigningPrimitiveProviderForBuild( + func() (NativeExecutionFFISigningPrimitive, error) { + return nil, expectedErr + }, + ) + if err != nil { + t.Fatalf("unexpected provider registration error: [%v]", err) + } + + defer func() { + recovered := recover() + if recovered == nil { + t.Fatal("expected panic") + } + + recoveredError, ok := recovered.(string) + if !ok { + t.Fatalf("unexpected panic type: [%T]", recovered) + } + + if !strings.Contains(recoveredError, expectedErr.Error()) { + t.Fatalf( + "unexpected panic value\nexpected substring: [%s]\nactual: [%v]", + expectedErr.Error(), + recovered, + ) + } + }() + + RegisterNativeExecutionFFISigningPrimitiveForBuild() +} diff --git a/pkg/frost/signing/native_ffi_primitive_registration_test.go b/pkg/frost/signing/native_ffi_primitive_registration_test.go new file mode 100644 index 0000000000..4c4f826317 --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_test.go @@ -0,0 +1,41 @@ +package signing + +import ( + "strings" + "testing" +) + +func TestRegisterNativeExecutionFFISigningPrimitiveProviderForBuild_Nil( + t *testing.T, +) { + err := RegisterNativeExecutionFFISigningPrimitiveProviderForBuild(nil) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains( + err.Error(), + "native execution FFI signing primitive provider is nil", + ) { + t.Fatalf( + "unexpected error\nexpected substring: [%s]\nactual: [%v]", + "native execution FFI signing primitive provider is nil", + err, + ) + } +} + +func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_DefaultBuildNoop( + t *testing.T, +) { + UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + RegisterNativeExecutionFFISigningPrimitiveForBuild() + + if currentNativeExecutionFFIExecutor() != nil { + t.Fatal("expected no FFI executor registration on default build") + } +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index df45f75c95..6d9abda544 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -133,6 +133,13 @@ func newNode( proposalGenerator CoordinationProposalGenerator, config Config, ) (*node, error) { + if err := RegisterSignerMaterialResolverForBuild(); err != nil { + return nil, fmt.Errorf( + "cannot register signer material resolver for build: %w", + err, + ) + } + if err := configureFrostSigningBackend(config); err != nil { return nil, fmt.Errorf("cannot configure FROST signing backend: %w", err) } diff --git a/pkg/tbtc/signer_material_resolver.go b/pkg/tbtc/signer_material_resolver.go index 246b5b6929..ce9dc08d06 100644 --- a/pkg/tbtc/signer_material_resolver.go +++ b/pkg/tbtc/signer_material_resolver.go @@ -14,6 +14,10 @@ type SignerMaterialResolver interface { ResolveSignerMaterial(privateKeyShare *tecdsa.PrivateKeyShare) (any, error) } +// SignerMaterialResolverProviderForBuild produces a signer material resolver +// bound to the current build/runtime flavor. +type SignerMaterialResolverProviderForBuild func() (SignerMaterialResolver, error) + type legacyPrivateKeyShareSignerMaterialResolver struct{} func (lpkssmr *legacyPrivateKeyShareSignerMaterialResolver) ResolveSignerMaterial( @@ -27,8 +31,9 @@ func (lpkssmr *legacyPrivateKeyShareSignerMaterialResolver) ResolveSignerMateria } var ( - signerMaterialResolverMutex sync.RWMutex - signerMaterialResolver SignerMaterialResolver = &legacyPrivateKeyShareSignerMaterialResolver{} + signerMaterialResolverMutex sync.RWMutex + signerMaterialResolver SignerMaterialResolver = &legacyPrivateKeyShareSignerMaterialResolver{} + signerMaterialResolverProviderForBuild SignerMaterialResolverProviderForBuild ) // RegisterSignerMaterialResolver registers a signer material resolver used by @@ -54,6 +59,32 @@ func UnregisterSignerMaterialResolver() { signerMaterialResolver = &legacyPrivateKeyShareSignerMaterialResolver{} } +// RegisterSignerMaterialResolverProviderForBuild registers a provider used by +// RegisterSignerMaterialResolverForBuild. +func RegisterSignerMaterialResolverProviderForBuild( + provider SignerMaterialResolverProviderForBuild, +) error { + if provider == nil { + return fmt.Errorf("signer material resolver provider is nil") + } + + signerMaterialResolverMutex.Lock() + defer signerMaterialResolverMutex.Unlock() + + signerMaterialResolverProviderForBuild = provider + + return nil +} + +// UnregisterSignerMaterialResolverProviderForBuild clears build-scoped resolver +// provider registration. +func UnregisterSignerMaterialResolverProviderForBuild() { + signerMaterialResolverMutex.Lock() + defer signerMaterialResolverMutex.Unlock() + + signerMaterialResolverProviderForBuild = nil +} + func currentSignerMaterialResolver() SignerMaterialResolver { signerMaterialResolverMutex.RLock() defer signerMaterialResolverMutex.RUnlock() @@ -61,6 +92,13 @@ func currentSignerMaterialResolver() SignerMaterialResolver { return signerMaterialResolver } +func currentSignerMaterialResolverProviderForBuild() SignerMaterialResolverProviderForBuild { + signerMaterialResolverMutex.RLock() + defer signerMaterialResolverMutex.RUnlock() + + return signerMaterialResolverProviderForBuild +} + func resolveSignerMaterial(privateKeyShare *tecdsa.PrivateKeyShare) (any, error) { resolver := currentSignerMaterialResolver() if resolver == nil { diff --git a/pkg/tbtc/signer_material_resolver_build.go b/pkg/tbtc/signer_material_resolver_build.go new file mode 100644 index 0000000000..115bd05b9d --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_build.go @@ -0,0 +1,7 @@ +package tbtc + +// RegisterSignerMaterialResolverForBuild attempts to register signer-material +// resolver bindings for the current build flavor. +func RegisterSignerMaterialResolverForBuild() error { + return registerSignerMaterialResolverForBuild() +} diff --git a/pkg/tbtc/signer_material_resolver_build_default.go b/pkg/tbtc/signer_material_resolver_build_default.go new file mode 100644 index 0000000000..a1d8cd7a23 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_build_default.go @@ -0,0 +1,7 @@ +//go:build !frost_native + +package tbtc + +func registerSignerMaterialResolverForBuild() error { + return nil +} diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native.go b/pkg/tbtc/signer_material_resolver_build_frost_native.go new file mode 100644 index 0000000000..2c6e32e5b8 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_build_frost_native.go @@ -0,0 +1,23 @@ +//go:build frost_native + +package tbtc + +import "fmt" + +func registerSignerMaterialResolverForBuild() error { + provider := currentSignerMaterialResolverProviderForBuild() + if provider == nil { + return nil + } + + resolver, err := provider() + if err != nil { + return err + } + + if resolver == nil { + return fmt.Errorf("signer material resolver is nil") + } + + return RegisterSignerMaterialResolver(resolver) +} diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go new file mode 100644 index 0000000000..ee03a562ce --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go @@ -0,0 +1,111 @@ +//go:build frost_native + +package tbtc + +import ( + "errors" + "testing" +) + +func TestRegisterSignerMaterialResolverForBuild_UsesRegisteredProvider( + t *testing.T, +) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + expected := []byte{0xaa, 0xbb} + err := RegisterSignerMaterialResolverProviderForBuild( + func() (SignerMaterialResolver, error) { + return &staticSignerMaterialResolver{ + result: expected, + }, nil + }, + ) + if err != nil { + t.Fatalf("unexpected provider registration error: [%v]", err) + } + + err = RegisterSignerMaterialResolverForBuild() + if err != nil { + t.Fatalf("unexpected build resolver registration error: [%v]", err) + } + + result, err := resolveSignerMaterial(createMockSigner(t).privateKeyShare) + if err != nil { + t.Fatalf("unexpected resolver error: [%v]", err) + } + + resultBytes, ok := result.([]byte) + if !ok { + t.Fatalf( + "unexpected resolved signer material type\nexpected: [%T]\nactual: [%T]", + []byte{}, + result, + ) + } + + if len(resultBytes) != len(expected) || + resultBytes[0] != expected[0] || + resultBytes[1] != expected[1] { + t.Fatalf( + "unexpected resolved signer material\nexpected: [%x]\nactual: [%x]", + expected, + resultBytes, + ) + } +} + +func TestRegisterSignerMaterialResolverForBuild_ProviderError(t *testing.T) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + expectedErr := errors.New("provider error") + err := RegisterSignerMaterialResolverProviderForBuild( + func() (SignerMaterialResolver, error) { + return nil, expectedErr + }, + ) + if err != nil { + t.Fatalf("unexpected provider registration error: [%v]", err) + } + + err = RegisterSignerMaterialResolverForBuild() + if err == nil { + t.Fatal("expected build resolver registration error") + } + + if !errors.Is(err, expectedErr) { + t.Fatalf( + "unexpected build resolver registration error\nexpected: [%v]\nactual: [%v]", + expectedErr, + err, + ) + } +} + +func TestRegisterSignerMaterialResolverForBuild_ProviderReturnsNilResolver( + t *testing.T, +) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + err := RegisterSignerMaterialResolverProviderForBuild( + func() (SignerMaterialResolver, error) { + return nil, nil + }, + ) + if err != nil { + t.Fatalf("unexpected provider registration error: [%v]", err) + } + + err = RegisterSignerMaterialResolverForBuild() + if err == nil { + t.Fatal("expected build resolver registration error") + } +} diff --git a/pkg/tbtc/signer_material_resolver_test.go b/pkg/tbtc/signer_material_resolver_test.go index 2a167e9ca0..49f8168ef2 100644 --- a/pkg/tbtc/signer_material_resolver_test.go +++ b/pkg/tbtc/signer_material_resolver_test.go @@ -25,6 +25,48 @@ func TestRegisterSignerMaterialResolver_Nil(t *testing.T) { } } +func TestRegisterSignerMaterialResolverProviderForBuild_Nil(t *testing.T) { + err := RegisterSignerMaterialResolverProviderForBuild(nil) + if err == nil { + t.Fatal("expected error") + } +} + +func TestRegisterSignerMaterialResolverForBuild_DefaultBuildNoop(t *testing.T) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + err := RegisterSignerMaterialResolverForBuild() + if err != nil { + t.Fatalf("unexpected build resolver registration error: [%v]", err) + } + + privateKeyShare := createMockSigner(t).privateKeyShare + result, err := resolveSignerMaterial(privateKeyShare) + if err != nil { + t.Fatalf("unexpected resolver error: [%v]", err) + } + + resolvedPrivateKeyShare, ok := result.(*tecdsa.PrivateKeyShare) + if !ok { + t.Fatalf( + "unexpected resolved signer material type\nexpected: [%T]\nactual: [%T]", + &tecdsa.PrivateKeyShare{}, + result, + ) + } + + if resolvedPrivateKeyShare != privateKeyShare { + t.Fatalf( + "unexpected resolved private key share\nexpected: [%v]\nactual: [%v]", + privateKeyShare, + resolvedPrivateKeyShare, + ) + } +} + func TestResolveSignerMaterial_DefaultResolver(t *testing.T) { UnregisterSignerMaterialResolver() t.Cleanup(UnregisterSignerMaterialResolver) From c3e8b02dc0a384cbf9119ea404c34dd9d220bdad Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 18:28:40 -0600 Subject: [PATCH 29/96] frost/native: wire default transitional provider integrations --- .../native_adapter_build_frost_native_test.go | 12 +- ...imitive_registration_default_build_test.go | 20 +++ ...ffi_primitive_registration_frost_native.go | 2 +- ...rimitive_registration_frost_native_test.go | 15 ++ .../native_ffi_primitive_registration_test.go | 15 -- ...ffi_primitive_transitional_frost_native.go | 115 ++++++++++++++ ...rimitive_transitional_frost_native_test.go | 143 ++++++++++++++++++ ...er_material_resolver_build_frost_native.go | 35 ++++- ...terial_resolver_build_frost_native_test.go | 65 ++++++++ ...er_material_resolver_default_build_test.go | 44 ++++++ pkg/tbtc/signer_material_resolver_test.go | 35 ----- ...igning_native_backend_frost_native_test.go | 89 ++++++++--- 12 files changed, 508 insertions(+), 82 deletions(-) create mode 100644 pkg/frost/signing/native_ffi_primitive_registration_default_build_test.go create mode 100644 pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go create mode 100644 pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go create mode 100644 pkg/tbtc/signer_material_resolver_default_build_test.go diff --git a/pkg/frost/signing/native_adapter_build_frost_native_test.go b/pkg/frost/signing/native_adapter_build_frost_native_test.go index 0bf861dba2..e8864a619c 100644 --- a/pkg/frost/signing/native_adapter_build_frost_native_test.go +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -57,10 +57,12 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { ResetExecutionBackend() UnregisterNativeExecutionAdapter() UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() RegisterNativeExecutionAdapterForBuild() t.Cleanup(ResetExecutionBackend) t.Cleanup(UnregisterNativeExecutionAdapter) t.Cleanup(UnregisterNativeExecutionBridge) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) err := SetExecutionBackendByName("native") if err != nil { @@ -91,19 +93,15 @@ func TestNativeExecutionBackend_FrostNativeBuildSelectable(t *testing.T) { } err = SetExecutionBackendByName("ffi") - if err == nil { - t.Fatal("expected strict ffi backend unavailable error") - } - - if !errors.Is(err, ErrNativeExecutionBackendUnavailable) { + if err != nil { t.Fatalf( - "unexpected ffi backend error\nexpected: [%v]\nactual: [%v]", - ErrNativeExecutionBackendUnavailable, + "unexpected strict ffi backend config error\nexpected: [nil]\nactual: [%v]", err, ) } UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() err = SetExecutionBackendByName("ffi") if err == nil { diff --git a/pkg/frost/signing/native_ffi_primitive_registration_default_build_test.go b/pkg/frost/signing/native_ffi_primitive_registration_default_build_test.go new file mode 100644 index 0000000000..6b492f8877 --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_default_build_test.go @@ -0,0 +1,20 @@ +//go:build !frost_native + +package signing + +import "testing" + +func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_DefaultBuildNoop( + t *testing.T, +) { + UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + RegisterNativeExecutionFFISigningPrimitiveForBuild() + + if currentNativeExecutionFFIExecutor() != nil { + t.Fatal("expected no FFI executor registration on default build") + } +} diff --git a/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go b/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go index b029d3bf6f..d6d3b3b3c8 100644 --- a/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go @@ -7,7 +7,7 @@ import "fmt" func registerNativeExecutionFFISigningPrimitiveForBuild() error { provider := currentNativeExecutionFFISigningPrimitiveProviderForBuild() if provider == nil { - return nil + provider = defaultNativeExecutionFFISigningPrimitiveProviderForBuild } primitive, err := provider() diff --git a/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go index af39c064cc..4259c01697 100644 --- a/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go @@ -36,6 +36,21 @@ func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_UsesProvider( } } +func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_UsesDefaultProvider( + t *testing.T, +) { + UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + RegisterNativeExecutionFFISigningPrimitiveForBuild() + + if currentNativeExecutionFFIExecutor() == nil { + t.Fatal("expected FFI executor registration from default build provider") + } +} + func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_ProviderErrorPanics( t *testing.T, ) { diff --git a/pkg/frost/signing/native_ffi_primitive_registration_test.go b/pkg/frost/signing/native_ffi_primitive_registration_test.go index 4c4f826317..6711b0b105 100644 --- a/pkg/frost/signing/native_ffi_primitive_registration_test.go +++ b/pkg/frost/signing/native_ffi_primitive_registration_test.go @@ -24,18 +24,3 @@ func TestRegisterNativeExecutionFFISigningPrimitiveProviderForBuild_Nil( ) } } - -func TestRegisterNativeExecutionFFISigningPrimitiveForBuild_DefaultBuildNoop( - t *testing.T, -) { - UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() - UnregisterNativeExecutionFFIExecutor() - t.Cleanup(UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild) - t.Cleanup(UnregisterNativeExecutionFFIExecutor) - - RegisterNativeExecutionFFISigningPrimitiveForBuild() - - if currentNativeExecutionFFIExecutor() != nil { - t.Fatal("expected no FFI executor registration on default build") - } -} diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go new file mode 100644 index 0000000000..f50f61ac94 --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -0,0 +1,115 @@ +//go:build frost_native + +package signing + +import ( + "context" + "fmt" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" + "github.com/keep-network/keep-core/pkg/tecdsa" + legacySigning "github.com/keep-network/keep-core/pkg/tecdsa/signing" +) + +func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( + NativeExecutionFFISigningPrimitive, + error, +) { + return &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{}, nil +} + +// buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive is a +// transitional primitive that consumes native signer material while executing +// legacy tECDSA signing under the hood. +type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} + +func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) Sign( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + if request == nil { + return nil, fmt.Errorf("request is nil") + } + + if request.Message == nil { + return nil, fmt.Errorf("request message is nil") + } + + privateKeyShare, err := decodeBuildTaggedLegacyPrivateKeyShare( + request.SignerMaterial, + ) + if err != nil { + return nil, err + } + + excludedMembersIndexes := []group.MemberIndex{} + if request.Attempt != nil { + excludedMembersIndexes = request.Attempt.ExcludedMembersIndexes + } + + legacyResult, err := legacySigning.Execute( + ctx, + logger, + request.Message, + request.SessionID, + request.MemberIndex, + privateKeyShare, + request.GroupSize, + request.DishonestThreshold, + excludedMembersIndexes, + request.Channel, + request.MembershipValidator, + ) + if err != nil { + return nil, err + } + + return FromTECDSASignature(legacyResult.Signature) +} + +func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { + legacySigning.RegisterUnmarshallers(channel) +} + +func decodeBuildTaggedLegacyPrivateKeyShare( + signerMaterial *NativeSignerMaterial, +) (*tecdsa.PrivateKeyShare, error) { + if signerMaterial == nil { + return nil, fmt.Errorf( + "%w: signer material is nil", + ErrNativeCryptographyUnavailable, + ) + } + + if signerMaterial.Format != NativeSignerMaterialFormatFrostUniFFIV1 { + return nil, fmt.Errorf( + "%w: unsupported signer material format: [%s]", + ErrNativeCryptographyUnavailable, + signerMaterial.Format, + ) + } + + if len(signerMaterial.Payload) == 0 { + return nil, fmt.Errorf( + "%w: signer material payload is empty", + ErrNativeCryptographyUnavailable, + ) + } + + privateKeyShare := &tecdsa.PrivateKeyShare{} + if err := privateKeyShare.Unmarshal(signerMaterial.Payload); err != nil { + return nil, fmt.Errorf( + "%w: cannot unmarshal signer material payload: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + return privateKeyShare, nil +} diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go new file mode 100644 index 0000000000..8e88ddce4a --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -0,0 +1,143 @@ +//go:build frost_native + +package signing + +import ( + "bytes" + "errors" + "math/big" + "testing" + + "github.com/keep-network/keep-core/pkg/internal/tecdsatest" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_ValidatesRequest( + t *testing.T, +) { + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err := primitive.Sign(nil, nil, nil) + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != "request is nil" { + t.Fatalf( + "unexpected error\nexpected: [%s]\nactual: [%v]", + "request is nil", + err, + ) + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_ValidatesMessage( + t *testing.T, +) { + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte{0x01}, + }, + }) + if err == nil { + t.Fatal("expected error") + } + + if err.Error() != "request message is nil" { + t.Fatalf( + "unexpected error\nexpected: [%s]\nactual: [%v]", + "request message is nil", + err, + ) + } +} + +func TestDecodeBuildTaggedLegacyPrivateKeyShare(t *testing.T) { + fixtures, err := tecdsatest.LoadPrivateKeyShareTestFixtures(5) + if err != nil { + t.Fatalf("failed loading key share fixtures: [%v]", err) + } + + expectedPrivateKeyShare := tecdsa.NewPrivateKeyShare(fixtures[0]) + expectedPayload, err := expectedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling private key share: [%v]", err) + } + + decodedPrivateKeyShare, err := decodeBuildTaggedLegacyPrivateKeyShare( + &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: expectedPayload, + }, + ) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + actualPayload, err := decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + + if !bytes.Equal(expectedPayload, actualPayload) { + t.Fatalf( + "unexpected decoded private key share\nexpected: [%x]\nactual: [%x]", + expectedPayload, + actualPayload, + ) + } +} + +func TestDecodeBuildTaggedLegacyPrivateKeyShare_RejectsInvalidMaterial( + t *testing.T, +) { + testCases := []struct { + name string + signerMaterial *NativeSignerMaterial + }{ + { + name: "nil signer material", + signerMaterial: nil, + }, + { + name: "unsupported format", + signerMaterial: &NativeSignerMaterial{ + Format: "other", + Payload: []byte{0x01}, + }, + }, + { + name: "empty payload", + signerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + }, + }, + { + name: "invalid payload", + signerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: big.NewInt(123).Bytes(), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := decodeBuildTaggedLegacyPrivateKeyShare(tc.signerMaterial) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + }) + } +} diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native.go b/pkg/tbtc/signer_material_resolver_build_frost_native.go index 2c6e32e5b8..fa78d1c1e3 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native.go @@ -2,12 +2,17 @@ package tbtc -import "fmt" +import ( + "fmt" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) func registerSignerMaterialResolverForBuild() error { provider := currentSignerMaterialResolverProviderForBuild() if provider == nil { - return nil + provider = defaultSignerMaterialResolverProviderForBuild } resolver, err := provider() @@ -21,3 +26,29 @@ func registerSignerMaterialResolverForBuild() error { return RegisterSignerMaterialResolver(resolver) } + +func defaultSignerMaterialResolverProviderForBuild() (SignerMaterialResolver, error) { + return &buildTaggedNativeSignerMaterialResolver{}, nil +} + +// buildTaggedNativeSignerMaterialResolver derives transitional native signer +// material from a legacy private key share for frost_native builds. +type buildTaggedNativeSignerMaterialResolver struct{} + +func (btnsmr *buildTaggedNativeSignerMaterialResolver) ResolveSignerMaterial( + privateKeyShare *tecdsa.PrivateKeyShare, +) (any, error) { + if privateKeyShare == nil { + return nil, fmt.Errorf("private key share is nil") + } + + payload, err := privateKeyShare.Marshal() + if err != nil { + return nil, fmt.Errorf("cannot marshal private key share: [%w]", err) + } + + return &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + Payload: payload, + }, nil +} diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go index ee03a562ce..886745464f 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go @@ -3,10 +3,75 @@ package tbtc import ( + "bytes" "errors" "testing" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" ) +func TestRegisterSignerMaterialResolverForBuild_UsesDefaultProvider( + t *testing.T, +) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + err := RegisterSignerMaterialResolverForBuild() + if err != nil { + t.Fatalf("unexpected build resolver registration error: [%v]", err) + } + + privateKeyShare := createMockSigner(t).privateKeyShare + + result, err := resolveSignerMaterial(privateKeyShare) + if err != nil { + t.Fatalf("unexpected resolver error: [%v]", err) + } + + nativeSignerMaterial, ok := result.(*frostsigning.NativeSignerMaterial) + if !ok { + t.Fatalf( + "unexpected resolved signer material type\nexpected: [%T]\nactual: [%T]", + &frostsigning.NativeSignerMaterial{}, + result, + ) + } + + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { + t.Fatalf( + "unexpected native signer material format\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + nativeSignerMaterial.Format, + ) + } + + decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} + if err := decodedPrivateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { + t.Fatalf("failed unmarshalling resolved signer payload: [%v]", err) + } + + expectedPayload, err := privateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling expected private key share: [%v]", err) + } + + actualPayload, err := decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + + if !bytes.Equal(expectedPayload, actualPayload) { + t.Fatalf( + "unexpected resolved signer payload\nexpected: [%x]\nactual: [%x]", + expectedPayload, + actualPayload, + ) + } +} + func TestRegisterSignerMaterialResolverForBuild_UsesRegisteredProvider( t *testing.T, ) { diff --git a/pkg/tbtc/signer_material_resolver_default_build_test.go b/pkg/tbtc/signer_material_resolver_default_build_test.go new file mode 100644 index 0000000000..c25489b72e --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_default_build_test.go @@ -0,0 +1,44 @@ +//go:build !frost_native + +package tbtc + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestRegisterSignerMaterialResolverForBuild_DefaultBuildNoop(t *testing.T) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + err := RegisterSignerMaterialResolverForBuild() + if err != nil { + t.Fatalf("unexpected build resolver registration error: [%v]", err) + } + + privateKeyShare := createMockSigner(t).privateKeyShare + result, err := resolveSignerMaterial(privateKeyShare) + if err != nil { + t.Fatalf("unexpected resolver error: [%v]", err) + } + + resolvedPrivateKeyShare, ok := result.(*tecdsa.PrivateKeyShare) + if !ok { + t.Fatalf( + "unexpected resolved signer material type\nexpected: [%T]\nactual: [%T]", + &tecdsa.PrivateKeyShare{}, + result, + ) + } + + if resolvedPrivateKeyShare != privateKeyShare { + t.Fatalf( + "unexpected resolved private key share\nexpected: [%v]\nactual: [%v]", + privateKeyShare, + resolvedPrivateKeyShare, + ) + } +} diff --git a/pkg/tbtc/signer_material_resolver_test.go b/pkg/tbtc/signer_material_resolver_test.go index 49f8168ef2..52ef802800 100644 --- a/pkg/tbtc/signer_material_resolver_test.go +++ b/pkg/tbtc/signer_material_resolver_test.go @@ -32,41 +32,6 @@ func TestRegisterSignerMaterialResolverProviderForBuild_Nil(t *testing.T) { } } -func TestRegisterSignerMaterialResolverForBuild_DefaultBuildNoop(t *testing.T) { - UnregisterSignerMaterialResolver() - UnregisterSignerMaterialResolverProviderForBuild() - t.Cleanup(UnregisterSignerMaterialResolver) - t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) - - err := RegisterSignerMaterialResolverForBuild() - if err != nil { - t.Fatalf("unexpected build resolver registration error: [%v]", err) - } - - privateKeyShare := createMockSigner(t).privateKeyShare - result, err := resolveSignerMaterial(privateKeyShare) - if err != nil { - t.Fatalf("unexpected resolver error: [%v]", err) - } - - resolvedPrivateKeyShare, ok := result.(*tecdsa.PrivateKeyShare) - if !ok { - t.Fatalf( - "unexpected resolved signer material type\nexpected: [%T]\nactual: [%T]", - &tecdsa.PrivateKeyShare{}, - result, - ) - } - - if resolvedPrivateKeyShare != privateKeyShare { - t.Fatalf( - "unexpected resolved private key share\nexpected: [%v]\nactual: [%v]", - privateKeyShare, - resolvedPrivateKeyShare, - ) - } -} - func TestResolveSignerMaterial_DefaultResolver(t *testing.T) { UnregisterSignerMaterialResolver() t.Cleanup(UnregisterSignerMaterialResolver) diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index 1d67eea981..862fec4302 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -15,21 +15,6 @@ import ( "github.com/keep-network/keep-core/pkg/net" ) -type noopNativeExecutionFFISigningPrimitive struct{} - -func (nnefsp *noopNativeExecutionFFISigningPrimitive) Sign( - ctx context.Context, - logger log.StandardLogger, - request *frostsigning.NativeExecutionFFISigningRequest, -) (*frost.Signature, error) { - return &frost.Signature{}, nil -} - -func (nnefsp *noopNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( - channel net.BroadcastChannel, -) { -} - type countingNativeExecutionFFISigningPrimitive struct { signCalls int } @@ -54,18 +39,12 @@ func TestConfigureFrostSigningBackend_FFIStrictConfigured_BuildAdapter(t *testin frostsigning.UnregisterNativeExecutionBridge() frostsigning.UnregisterNativeExecutionFFIExecutor() frostsigning.RegisterNativeExecutionAdapterForBuild() - err := frostsigning.RegisterNativeExecutionFFISigningPrimitive( - &noopNativeExecutionFFISigningPrimitive{}, - ) - if err != nil { - t.Fatalf("unexpected native FFI primitive registration error: [%v]", err) - } t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) - err = configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) if err != nil { t.Fatalf("unexpected strict ffi backend configuration error: [%v]", err) } @@ -172,6 +151,72 @@ func TestSigningExecutor_Sign_NativeBackend(t *testing.T) { } } +func TestSigningExecutor_Sign_FFIStrictBackend_WithNativeSignerMaterial( + t *testing.T, +) { + executor := setupSigningExecutor(t) + + for _, signer := range executor.signers { + payload, err := signer.privateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling signer private key share: [%v]", err) + } + + signer.signerMaterial = &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + Payload: payload, + } + } + + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.UnregisterNativeExecutionBridge() + frostsigning.UnregisterNativeExecutionFFIExecutor() + frostsigning.RegisterNativeExecutionAdapterForBuild() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) + + err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + if err != nil { + t.Fatalf("unexpected strict ffi backend config error: [%v]", err) + } + + if frostsigning.CurrentExecutionBackendName() != frostsigning.NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeExecutionBackendName, + frostsigning.CurrentExecutionBackendName(), + ) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + message := big.NewInt(100) + startBlock := uint64(0) + + signature, _, endBlock, err := executor.sign(ctx, message, startBlock) + if err != nil { + t.Fatalf("unexpected strict ffi signing error: [%v]", err) + } + + walletPublicKey := executor.wallet().publicKey + if !ecdsa.Verify( + walletPublicKey, + message.Bytes(), + new(big.Int).SetBytes(signature.R[:]), + new(big.Int).SetBytes(signature.S[:]), + ) { + t.Fatalf("invalid signature: [%+v]", signature) + } + + if endBlock <= startBlock { + t.Fatal("wrong end block") + } +} + func TestSigningExecutor_Sign_NativeBackend_FallsBackWhenOnlyLegacySignerMaterial( t *testing.T, ) { From 9aa474c83061c64ff5355c3428a3f31342671860 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 18:32:03 -0600 Subject: [PATCH 30/96] tbtc: resolve legacy signer material through build resolver on load --- pkg/tbtc/signer_material_encoding.go | 16 +++- ...ner_material_encoding_frost_native_test.go | 76 +++++++++++++++++++ ...igning_native_backend_frost_native_test.go | 18 ++--- 3 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 pkg/tbtc/signer_material_encoding_frost_native_test.go diff --git a/pkg/tbtc/signer_material_encoding.go b/pkg/tbtc/signer_material_encoding.go index 4665d95a22..46f1191ffb 100644 --- a/pkg/tbtc/signer_material_encoding.go +++ b/pkg/tbtc/signer_material_encoding.go @@ -80,8 +80,22 @@ func unmarshalSignerMaterialFromPersistence( return nil, fmt.Errorf("cannot unmarshal private key share: [%w]", err) } + resolvedSignerMaterial, err := resolveSignerMaterial(privateKeyShare) + if err != nil { + return nil, fmt.Errorf( + "cannot resolve signer material from legacy private key share: [%w]", + err, + ) + } + + if resolvedSignerMaterial == nil { + return nil, fmt.Errorf( + "resolved signer material from legacy private key share is nil", + ) + } + return &unmarshaledSignerMaterial{ - signerMaterial: privateKeyShare, + signerMaterial: resolvedSignerMaterial, privateKeyShare: privateKeyShare, }, nil } diff --git a/pkg/tbtc/signer_material_encoding_frost_native_test.go b/pkg/tbtc/signer_material_encoding_frost_native_test.go new file mode 100644 index 0000000000..8a4782965e --- /dev/null +++ b/pkg/tbtc/signer_material_encoding_frost_native_test.go @@ -0,0 +1,76 @@ +//go:build frost_native + +package tbtc + +import ( + "bytes" + "testing" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestUnmarshalSignerMaterialFromPersistence_LegacyEncodingResolvesNativeMaterialOnFrostNativeBuild( + t *testing.T, +) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + if err := RegisterSignerMaterialResolverForBuild(); err != nil { + t.Fatalf("unexpected build resolver registration error: [%v]", err) + } + + privateKeyShare := createMockSigner(t).privateKeyShare + legacyEncoded, err := privateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling legacy private key share: [%v]", err) + } + + unmarshaledSignerMaterial, err := unmarshalSignerMaterialFromPersistence( + legacyEncoded, + ) + if err != nil { + t.Fatalf("unexpected unmarshal error: [%v]", err) + } + + if unmarshaledSignerMaterial.privateKeyShare == nil { + t.Fatal("expected legacy private key share to be preserved") + } + + nativeSignerMaterial, ok := unmarshaledSignerMaterial.signerMaterial.(*frostsigning.NativeSignerMaterial) + if !ok { + t.Fatalf( + "unexpected resolved signer material type\nexpected: [%T]\nactual: [%T]", + &frostsigning.NativeSignerMaterial{}, + unmarshaledSignerMaterial.signerMaterial, + ) + } + + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { + t.Fatalf( + "unexpected signer material format\nexpected: [%v]\nactual: [%v]", + frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + nativeSignerMaterial.Format, + ) + } + + decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} + if err := decodedPrivateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { + t.Fatalf("failed unmarshalling native signer material payload: [%v]", err) + } + + actualPayload, err := decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + + if !bytes.Equal(actualPayload, legacyEncoded) { + t.Fatalf( + "unexpected resolved signer payload\nexpected: [%x]\nactual: [%x]", + legacyEncoded, + actualPayload, + ) + } +} diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index 862fec4302..ed8c2dfec9 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -156,18 +156,6 @@ func TestSigningExecutor_Sign_FFIStrictBackend_WithNativeSignerMaterial( ) { executor := setupSigningExecutor(t) - for _, signer := range executor.signers { - payload, err := signer.privateKeyShare.Marshal() - if err != nil { - t.Fatalf("failed marshaling signer private key share: [%v]", err) - } - - signer.signerMaterial = &frostsigning.NativeSignerMaterial{ - Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, - Payload: payload, - } - } - frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() frostsigning.UnregisterNativeExecutionBridge() @@ -222,6 +210,12 @@ func TestSigningExecutor_Sign_NativeBackend_FallsBackWhenOnlyLegacySignerMateria ) { executor := setupSigningExecutor(t) + // Force legacy-only signer material to exercise fallback classification + // behavior even when frost_native build defaults resolve to native material. + for _, signer := range executor.signers { + signer.signerMaterial = signer.privateKeyShare + } + primitive := &countingNativeExecutionFFISigningPrimitive{} frostsigning.ResetExecutionBackend() From 245c64cf339e9f6a6c0d93d0cc1f283965385abd Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 18:33:59 -0600 Subject: [PATCH 31/96] tbtc: add frost-native legacy roundtrip migration test --- ...ner_material_encoding_frost_native_test.go | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/pkg/tbtc/signer_material_encoding_frost_native_test.go b/pkg/tbtc/signer_material_encoding_frost_native_test.go index 8a4782965e..9d624b1807 100644 --- a/pkg/tbtc/signer_material_encoding_frost_native_test.go +++ b/pkg/tbtc/signer_material_encoding_frost_native_test.go @@ -7,7 +7,9 @@ import ( "testing" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tbtc/gen/pb" "github.com/keep-network/keep-core/pkg/tecdsa" + "google.golang.org/protobuf/proto" ) func TestUnmarshalSignerMaterialFromPersistence_LegacyEncodingResolvesNativeMaterialOnFrostNativeBuild( @@ -74,3 +76,60 @@ func TestUnmarshalSignerMaterialFromPersistence_LegacyEncodingResolvesNativeMate ) } } + +func TestSignerMarshalling_LegacyRoundtripMigratesToNativeEnvelopeOnFrostNativeBuild( + t *testing.T, +) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + if err := RegisterSignerMaterialResolverForBuild(); err != nil { + t.Fatalf("unexpected build resolver registration error: [%v]", err) + } + + legacySigner := createMockSigner(t) + legacySigner.signerMaterial = legacySigner.privateKeyShare + + initialEncodedSigner, err := legacySigner.Marshal() + if err != nil { + t.Fatalf("unexpected initial signer marshal error: [%v]", err) + } + + initialPBSigner := &pb.Signer{} + if err := proto.Unmarshal(initialEncodedSigner, initialPBSigner); err != nil { + t.Fatalf("unexpected initial proto unmarshal error: [%v]", err) + } + + if bytes.HasPrefix(initialPBSigner.PrivateKeyShare, signerMaterialEnvelopePrefix) { + t.Fatal("expected initial legacy signer encoding without native envelope") + } + + unmarshaledSigner := &signer{} + if err := unmarshaledSigner.Unmarshal(initialEncodedSigner); err != nil { + t.Fatalf("unexpected signer unmarshal error: [%v]", err) + } + + if _, ok := unmarshaledSigner.signerMaterial.(*frostsigning.NativeSignerMaterial); !ok { + t.Fatalf( + "unexpected signer material type after legacy unmarshal\nexpected: [%T]\nactual: [%T]", + &frostsigning.NativeSignerMaterial{}, + unmarshaledSigner.signerMaterial, + ) + } + + migratedEncodedSigner, err := unmarshaledSigner.Marshal() + if err != nil { + t.Fatalf("unexpected migrated signer marshal error: [%v]", err) + } + + migratedPBSigner := &pb.Signer{} + if err := proto.Unmarshal(migratedEncodedSigner, migratedPBSigner); err != nil { + t.Fatalf("unexpected migrated proto unmarshal error: [%v]", err) + } + + if !bytes.HasPrefix(migratedPBSigner.PrivateKeyShare, signerMaterialEnvelopePrefix) { + t.Fatal("expected migrated signer encoding with native envelope prefix") + } +} From 80503964e11f97ae742cd414ebf57ad210b91c6c Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 19:12:02 -0600 Subject: [PATCH 32/96] tbtc: recover legacy key share from native envelope on load --- pkg/tbtc/signer_material_encoding.go | 27 +++++++++- ...er_material_encoding_default_build_test.go | 52 +++++++++++++++++++ pkg/tbtc/signer_material_encoding_test.go | 33 ++++++++++-- 3 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 pkg/tbtc/signer_material_encoding_default_build_test.go diff --git a/pkg/tbtc/signer_material_encoding.go b/pkg/tbtc/signer_material_encoding.go index 46f1191ffb..4b13f5e492 100644 --- a/pkg/tbtc/signer_material_encoding.go +++ b/pkg/tbtc/signer_material_encoding.go @@ -49,6 +49,8 @@ func marshalSignerMaterialForPersistence( material.Payload, ) case []byte: + // Transitional compatibility: raw bytes are treated as + // frost-uniffi-v1 payloads produced by default resolver paths. return encodeNativeSignerMaterialForPersistence( frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, material, @@ -69,9 +71,13 @@ func unmarshalSignerMaterialFromPersistence( } if isNative { + privateKeyShare := legacyPrivateKeyShareFromNativeSignerMaterial( + nativeSignerMaterial, + ) + return &unmarshaledSignerMaterial{ signerMaterial: nativeSignerMaterial, - privateKeyShare: nil, + privateKeyShare: privateKeyShare, }, nil } @@ -218,3 +224,22 @@ func readPersistenceUvarint(data []byte, offset int) (uint64, int, error) { return value, lengthBytes, nil } + +func legacyPrivateKeyShareFromNativeSignerMaterial( + nativeSignerMaterial *frostsigning.NativeSignerMaterial, +) *tecdsa.PrivateKeyShare { + if nativeSignerMaterial == nil { + return nil + } + + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { + return nil + } + + privateKeyShare := &tecdsa.PrivateKeyShare{} + if err := privateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { + return nil + } + + return privateKeyShare +} diff --git a/pkg/tbtc/signer_material_encoding_default_build_test.go b/pkg/tbtc/signer_material_encoding_default_build_test.go new file mode 100644 index 0000000000..031d28477e --- /dev/null +++ b/pkg/tbtc/signer_material_encoding_default_build_test.go @@ -0,0 +1,52 @@ +//go:build !frost_native + +package tbtc + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestUnmarshalSignerMaterialFromPersistence_LegacyEncoding_DefaultBuildReturnsLegacySignerMaterial( + t *testing.T, +) { + UnregisterSignerMaterialResolver() + UnregisterSignerMaterialResolverProviderForBuild() + t.Cleanup(UnregisterSignerMaterialResolver) + t.Cleanup(UnregisterSignerMaterialResolverProviderForBuild) + + if err := RegisterSignerMaterialResolverForBuild(); err != nil { + t.Fatalf("unexpected build resolver registration error: [%v]", err) + } + + privateKeyShare := createMockSigner(t).privateKeyShare + legacyEncoded, err := privateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling legacy private key share: [%v]", err) + } + + unmarshaledSignerMaterial, err := unmarshalSignerMaterialFromPersistence( + legacyEncoded, + ) + if err != nil { + t.Fatalf("unexpected unmarshal error: [%v]", err) + } + + if unmarshaledSignerMaterial.privateKeyShare == nil { + t.Fatal("expected private key share") + } + + resolvedPrivateKeyShare, ok := unmarshaledSignerMaterial.signerMaterial.(*tecdsa.PrivateKeyShare) + if !ok { + t.Fatalf( + "unexpected signer material type\nexpected: [%T]\nactual: [%T]", + &tecdsa.PrivateKeyShare{}, + unmarshaledSignerMaterial.signerMaterial, + ) + } + + if resolvedPrivateKeyShare != unmarshaledSignerMaterial.privateKeyShare { + t.Fatal("expected signer material to reference recovered private key share") + } +} diff --git a/pkg/tbtc/signer_material_encoding_test.go b/pkg/tbtc/signer_material_encoding_test.go index 1051c4e666..2f83fe87e4 100644 --- a/pkg/tbtc/signer_material_encoding_test.go +++ b/pkg/tbtc/signer_material_encoding_test.go @@ -84,9 +84,15 @@ func TestMarshalSignerMaterialForPersistence_NativeSignerMaterial(t *testing.T) } func TestUnmarshalSignerMaterialFromPersistence_NativeEnvelope(t *testing.T) { + signer := createMockSigner(t) + payload, err := signer.privateKeyShare.Marshal() + if err != nil { + t.Fatalf("unexpected private key share marshal error: [%v]", err) + } + encoded, err := encodeNativeSignerMaterialForPersistence( frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, - []byte{0x10, 0x20}, + payload, ) if err != nil { t.Fatalf("unexpected encode error: [%v]", err) @@ -97,8 +103,21 @@ func TestUnmarshalSignerMaterialFromPersistence_NativeEnvelope(t *testing.T) { t.Fatalf("unexpected unmarshal error: [%v]", err) } - if decoded.privateKeyShare != nil { - t.Fatal("expected nil private key share for native signer material") + if decoded.privateKeyShare == nil { + t.Fatal("expected legacy private key share recovery from native signer material") + } + + recoveredPayload, err := decoded.privateKeyShare.Marshal() + if err != nil { + t.Fatalf("unexpected recovered private key share marshal error: [%v]", err) + } + + if !bytes.Equal(recoveredPayload, payload) { + t.Fatalf( + "unexpected recovered private key share\nexpected: [%x]\nactual: [%x]", + payload, + recoveredPayload, + ) } nativeSignerMaterial, ok := decoded.signerMaterial.(*frostsigning.NativeSignerMaterial) @@ -117,6 +136,14 @@ func TestUnmarshalSignerMaterialFromPersistence_NativeEnvelope(t *testing.T) { nativeSignerMaterial.Format, ) } + + if !bytes.Equal(nativeSignerMaterial.Payload, payload) { + t.Fatalf( + "unexpected signer material payload\nexpected: [%x]\nactual: [%x]", + payload, + nativeSignerMaterial.Payload, + ) + } } func TestUnmarshalSignerMaterialFromPersistence_CorruptedNativeEnvelope(t *testing.T) { From 6532456d57fc8732558836230b61401520a95c75 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 19:28:53 -0600 Subject: [PATCH 33/96] tbtc: synchronize signing-done state and retransmission backoff ticks --- pkg/net/retransmission/strategy.go | 10 +++- pkg/tbtc/signing_done.go | 80 ++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/pkg/net/retransmission/strategy.go b/pkg/net/retransmission/strategy.go index fd50384fb2..cbf30bc433 100644 --- a/pkg/net/retransmission/strategy.go +++ b/pkg/net/retransmission/strategy.go @@ -1,6 +1,10 @@ package retransmission -import "github.com/keep-network/keep-core/pkg/net" +import ( + "sync" + + "github.com/keep-network/keep-core/pkg/net" +) // Strategy represents a specific retransmission strategy. type Strategy interface { @@ -44,6 +48,7 @@ func (ss *StandardStrategy) Tick(retransmitFn RetransmitFn) error { // ticks, between third and fourth is 4 ticks and so on. Graphically, the // schedule looks as follows: R _ R _ _ R _ _ _ _ R _ _ _ _ _ _ _ _ R type BackoffStrategy struct { + mutex sync.Mutex tickCounter uint64 delay uint64 retransmitTick uint64 @@ -61,6 +66,9 @@ func WithBackoffStrategy() *BackoffStrategy { // Tick implements the Strategy.Tick function. func (bos *BackoffStrategy) Tick(retransmitFn RetransmitFn) error { + bos.mutex.Lock() + defer bos.mutex.Unlock() + bos.tickCounter++ if bos.tickCounter == bos.retransmitTick { diff --git a/pkg/tbtc/signing_done.go b/pkg/tbtc/signing_done.go index 1b49c51ee5..f14426d87f 100644 --- a/pkg/tbtc/signing_done.go +++ b/pkg/tbtc/signing_done.go @@ -54,7 +54,7 @@ type signingDoneCheck struct { cancelReceiveCtx context.CancelFunc expectedSignersCount int doneSigners map[group.MemberIndex]*signingDoneMessage - doneSignersMutex sync.Mutex + doneSignersMutex sync.RWMutex } func newSigningDoneCheck( @@ -90,14 +90,16 @@ func (sdc *signingDoneCheck) listen( // causes warnings on the channel level. sdc.receiveCtx, sdc.cancelReceiveCtx = context.WithCancel(ctx) + sdc.doneSignersMutex.Lock() + sdc.expectedSignersCount = len(attemptMembersIndexes) + sdc.doneSigners = make(map[group.MemberIndex]*signingDoneMessage) + sdc.doneSignersMutex.Unlock() + messagesChan := make(chan net.Message, signingDoneReceiveBuffer) sdc.broadcastChannel.Recv(sdc.receiveCtx, func(message net.Message) { messagesChan <- message }) - sdc.expectedSignersCount = len(attemptMembersIndexes) - sdc.doneSigners = make(map[group.MemberIndex]*signingDoneMessage) - go func() { for { select { @@ -117,9 +119,9 @@ func (sdc *signingDoneCheck) listen( continue } - sdc.doneSignersMutex.Lock() - sdc.doneSigners[doneMessage.senderID] = doneMessage - sdc.doneSignersMutex.Unlock() + if !sdc.recordDoneMessage(doneMessage) { + continue + } case <-sdc.receiveCtx.Done(): return @@ -169,11 +171,12 @@ func (sdc *signingDoneCheck) waitUntilAllDone(ctx context.Context) ( return nil, 0, errWaitDoneTimedOut case <-ticker.C: - if sdc.expectedSignersCount == len(sdc.doneSigners) { + expectedSignersCount, doneSigners := sdc.snapshotDoneSigners() + if expectedSignersCount == len(doneSigners) { var signature *frost.Signature var latestEndBlock uint64 - for _, doneMessage := range sdc.doneSigners { + for _, doneMessage := range doneSigners { if signature == nil { signature = doneMessage.signature } else { @@ -206,12 +209,6 @@ func (sdc *signingDoneCheck) isValidDoneMessage( attemptNumber uint64, attemptTimeoutBlock uint64, ) bool { - _, signerDone := sdc.doneSigners[doneMessage.senderID] - if signerDone { - // only one done message allowed - return false - } - if !sdc.membershipValidator.IsValidMembership( doneMessage.senderID, senderPublicKey, @@ -237,3 +234,56 @@ func (sdc *signingDoneCheck) isValidDoneMessage( return true } + +func (sdc *signingDoneCheck) recordDoneMessage( + doneMessage *signingDoneMessage, +) bool { + sdc.doneSignersMutex.Lock() + defer sdc.doneSignersMutex.Unlock() + + if _, signerDone := sdc.doneSigners[doneMessage.senderID]; signerDone { + // Only one done message is allowed for the given signer. + return false + } + + sdc.doneSigners[doneMessage.senderID] = doneMessage.clone() + return true +} + +func (sdc *signingDoneCheck) snapshotDoneSigners() ( + int, + []*signingDoneMessage, +) { + sdc.doneSignersMutex.RLock() + defer sdc.doneSignersMutex.RUnlock() + + result := make([]*signingDoneMessage, 0, len(sdc.doneSigners)) + for _, doneMessage := range sdc.doneSigners { + result = append(result, doneMessage.clone()) + } + + return sdc.expectedSignersCount, result +} + +func (sdm *signingDoneMessage) clone() *signingDoneMessage { + if sdm == nil { + return nil + } + + result := &signingDoneMessage{ + senderID: sdm.senderID, + attemptNumber: sdm.attemptNumber, + endBlock: sdm.endBlock, + } + + if sdm.message != nil { + result.message = new(big.Int).Set(sdm.message) + } + + if sdm.signature != nil { + signatureCopy := *sdm.signature + result.signature = &signatureCopy + } + + return result +} From 8ef50715de9cbc83e4d7d33b55acaa0ad7fd6417 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 20 Feb 2026 20:07:06 -0600 Subject: [PATCH 34/96] frost/native: add v2 native round-signing protocol path --- ...ffi_primitive_transitional_frost_native.go | 47 +- .../native_frost_engine_frost_native.go | 105 +++ .../native_frost_protocol_frost_native.go | 621 ++++++++++++++++++ ...native_frost_protocol_frost_native_test.go | 329 ++++++++++ 4 files changed, 1100 insertions(+), 2 deletions(-) create mode 100644 pkg/frost/signing/native_frost_engine_frost_native.go create mode 100644 pkg/frost/signing/native_frost_protocol_frost_native.go create mode 100644 pkg/frost/signing/native_frost_protocol_frost_native_test.go diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index f50f61ac94..28eaa7dd08 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -22,8 +22,9 @@ func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( } // buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive is a -// transitional primitive that consumes native signer material while executing -// legacy tECDSA signing under the hood. +// transitional primitive that executes native two-round FROST when +// `frost-uniffi-v2` signer material is provided, and preserves legacy bridge +// execution for `frost-uniffi-v1` payloads. type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) Sign( @@ -39,6 +40,47 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) return nil, fmt.Errorf("request message is nil") } + if request.SignerMaterial == nil { + return nil, fmt.Errorf( + "%w: signer material is nil", + ErrNativeCryptographyUnavailable, + ) + } + + switch request.SignerMaterial.Format { + case NativeSignerMaterialFormatFrostUniFFIV2: + nativeSignerMaterial, err := decodeNativeFROSTUniFFIV2SignerMaterial( + request.SignerMaterial, + ) + if err != nil { + return nil, err + } + + return executeNativeFROSTSigning( + ctx, + logger, + request, + currentNativeFROSTSigningEngine(), + nativeSignerMaterial, + ) + + case NativeSignerMaterialFormatFrostUniFFIV1: + return btlcnnefsp.signWithLegacyTECDSABridge(ctx, logger, request) + + default: + return nil, fmt.Errorf( + "%w: unsupported signer material format: [%s]", + ErrNativeCryptographyUnavailable, + request.SignerMaterial.Format, + ) + } +} + +func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithLegacyTECDSABridge( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { privateKeyShare, err := decodeBuildTaggedLegacyPrivateKeyShare( request.SignerMaterial, ) @@ -74,6 +116,7 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( channel net.BroadcastChannel, ) { + registerNativeFROSTSigningUnmarshallers(channel) legacySigning.RegisterUnmarshallers(channel) } diff --git a/pkg/frost/signing/native_frost_engine_frost_native.go b/pkg/frost/signing/native_frost_engine_frost_native.go new file mode 100644 index 0000000000..757212b0e5 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_frost_native.go @@ -0,0 +1,105 @@ +//go:build frost_native + +package signing + +import ( + "fmt" +) + +const ( + // NativeSignerMaterialFormatFrostUniFFIV2 carries fully-native signer + // material required to execute two-round FROST signing. + NativeSignerMaterialFormatFrostUniFFIV2 = "frost-uniffi-v2" +) + +var nativeFROSTSigningEngine NativeFROSTSigningEngine + +// NativeFROSTKeyPackage carries native key-package bytes and participant +// identifier expected by the native FROST engine. +type NativeFROSTKeyPackage struct { + Identifier string `json:"identifier"` + Data []byte `json:"data"` +} + +// NativeFROSTPublicKeyPackage carries native public-key-package payload. +type NativeFROSTPublicKeyPackage struct { + VerifyingShares map[string]string `json:"verifyingShares"` + VerifyingKey string `json:"verifyingKey"` +} + +// NativeFROSTNonces is round-one signer-local nonce material. +type NativeFROSTNonces struct { + Data []byte `json:"data"` +} + +// NativeFROSTCommitment is round-one commitment shared with the group. +type NativeFROSTCommitment struct { + Identifier string `json:"identifier"` + Data []byte `json:"data"` +} + +// NativeFROSTSigningPackage is coordinator-computed package used in round two. +type NativeFROSTSigningPackage struct { + Data []byte `json:"data"` +} + +// NativeFROSTSignatureShare is round-two signature share. +type NativeFROSTSignatureShare struct { + Identifier string `json:"identifier"` + Data []byte `json:"data"` +} + +// NativeFROSTSigningEngine executes cryptographic round operations needed by +// the native FROST signing protocol. +type NativeFROSTSigningEngine interface { + GenerateNoncesAndCommitments( + keyPackage *NativeFROSTKeyPackage, + ) (*NativeFROSTNonces, *NativeFROSTCommitment, error) + NewSigningPackage( + message []byte, + commitments []*NativeFROSTCommitment, + ) (*NativeFROSTSigningPackage, error) + Sign( + signingPackage *NativeFROSTSigningPackage, + nonces *NativeFROSTNonces, + keyPackage *NativeFROSTKeyPackage, + ) (*NativeFROSTSignatureShare, error) + Aggregate( + signingPackage *NativeFROSTSigningPackage, + signatureShares []*NativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, + ) ([]byte, error) +} + +// RegisterNativeFROSTSigningEngine registers the native FROST cryptographic +// engine used by the tagged native-signing primitive. +func RegisterNativeFROSTSigningEngine( + engine NativeFROSTSigningEngine, +) error { + if engine == nil { + return fmt.Errorf("native FROST signing engine is nil") + } + + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeFROSTSigningEngine = engine + + return nil +} + +// UnregisterNativeFROSTSigningEngine clears native FROST signing engine +// registration. +func UnregisterNativeFROSTSigningEngine() { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeFROSTSigningEngine = nil +} + +func currentNativeFROSTSigningEngine() NativeFROSTSigningEngine { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return nativeFROSTSigningEngine +} diff --git a/pkg/frost/signing/native_frost_protocol_frost_native.go b/pkg/frost/signing/native_frost_protocol_frost_native.go new file mode 100644 index 0000000000..08104e5a96 --- /dev/null +++ b/pkg/frost/signing/native_frost_protocol_frost_native.go @@ -0,0 +1,621 @@ +//go:build frost_native + +package signing + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +const nativeFROSTMessageTypePrefix = "frost_signing/native_frost/" + +type nativeFROSTUniFFIV2SignerMaterial struct { + KeyPackage *NativeFROSTKeyPackage `json:"keyPackage"` + PublicKeyPackage *NativeFROSTPublicKeyPackage `json:"publicKeyPackage"` +} + +func (nufv2sm *nativeFROSTUniFFIV2SignerMaterial) validate() error { + if nufv2sm == nil { + return fmt.Errorf("native signer material payload is nil") + } + + if nufv2sm.KeyPackage == nil { + return fmt.Errorf("native signer key package is nil") + } + + if nufv2sm.KeyPackage.Identifier == "" { + return fmt.Errorf("native signer key package identifier is empty") + } + + if len(nufv2sm.KeyPackage.Data) == 0 { + return fmt.Errorf("native signer key package data is empty") + } + + if nufv2sm.PublicKeyPackage == nil { + return fmt.Errorf("native signer public key package is nil") + } + + if nufv2sm.PublicKeyPackage.VerifyingKey == "" { + return fmt.Errorf("native signer public key package verifying key is empty") + } + + return nil +} + +func decodeNativeFROSTUniFFIV2SignerMaterial( + signerMaterial *NativeSignerMaterial, +) (*nativeFROSTUniFFIV2SignerMaterial, error) { + if signerMaterial == nil { + return nil, fmt.Errorf( + "%w: signer material is nil", + ErrNativeCryptographyUnavailable, + ) + } + + if signerMaterial.Format != NativeSignerMaterialFormatFrostUniFFIV2 { + return nil, fmt.Errorf( + "%w: unsupported signer material format: [%s]", + ErrNativeCryptographyUnavailable, + signerMaterial.Format, + ) + } + + if len(signerMaterial.Payload) == 0 { + return nil, fmt.Errorf( + "%w: signer material payload is empty", + ErrNativeCryptographyUnavailable, + ) + } + + var decoded nativeFROSTUniFFIV2SignerMaterial + if err := json.Unmarshal(signerMaterial.Payload, &decoded); err != nil { + return nil, fmt.Errorf( + "%w: cannot unmarshal native signer material payload: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if err := decoded.validate(); err != nil { + return nil, fmt.Errorf( + "%w: invalid native signer material payload: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + return &decoded, nil +} + +type nativeFROSTRoundOneCommitmentMessage struct { + SenderIDValue uint32 `json:"senderID"` + SessionIDValue string `json:"sessionID"` + ParticipantIdentifier string `json:"participantIdentifier"` + CommitmentData []byte `json:"commitmentData"` +} + +func (nfr1cm *nativeFROSTRoundOneCommitmentMessage) SenderID() group.MemberIndex { + return group.MemberIndex(nfr1cm.SenderIDValue) +} + +func (nfr1cm *nativeFROSTRoundOneCommitmentMessage) SessionID() string { + return nfr1cm.SessionIDValue +} + +func (nfr1cm *nativeFROSTRoundOneCommitmentMessage) Type() string { + return nativeFROSTMessageTypePrefix + "round_one_commitment" +} + +func (nfr1cm *nativeFROSTRoundOneCommitmentMessage) Marshal() ([]byte, error) { + return json.Marshal(nfr1cm) +} + +func (nfr1cm *nativeFROSTRoundOneCommitmentMessage) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, nfr1cm); err != nil { + return err + } + + if nfr1cm.SenderID() == 0 { + return fmt.Errorf("sender ID is zero") + } + + if nfr1cm.SessionID() == "" { + return fmt.Errorf("session ID is empty") + } + + if nfr1cm.ParticipantIdentifier == "" { + return fmt.Errorf("participant identifier is empty") + } + + if len(nfr1cm.CommitmentData) == 0 { + return fmt.Errorf("commitment data is empty") + } + + return nil +} + +type nativeFROSTRoundTwoSignatureShareMessage struct { + SenderIDValue uint32 `json:"senderID"` + SessionIDValue string `json:"sessionID"` + ParticipantIdentifier string `json:"participantIdentifier"` + SignatureShareData []byte `json:"signatureShareData"` +} + +func (nfr2ssm *nativeFROSTRoundTwoSignatureShareMessage) SenderID() group.MemberIndex { + return group.MemberIndex(nfr2ssm.SenderIDValue) +} + +func (nfr2ssm *nativeFROSTRoundTwoSignatureShareMessage) SessionID() string { + return nfr2ssm.SessionIDValue +} + +func (nfr2ssm *nativeFROSTRoundTwoSignatureShareMessage) Type() string { + return nativeFROSTMessageTypePrefix + "round_two_signature_share" +} + +func (nfr2ssm *nativeFROSTRoundTwoSignatureShareMessage) Marshal() ([]byte, error) { + return json.Marshal(nfr2ssm) +} + +func (nfr2ssm *nativeFROSTRoundTwoSignatureShareMessage) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, nfr2ssm); err != nil { + return err + } + + if nfr2ssm.SenderID() == 0 { + return fmt.Errorf("sender ID is zero") + } + + if nfr2ssm.SessionID() == "" { + return fmt.Errorf("session ID is empty") + } + + if nfr2ssm.ParticipantIdentifier == "" { + return fmt.Errorf("participant identifier is empty") + } + + if len(nfr2ssm.SignatureShareData) == 0 { + return fmt.Errorf("signature share data is empty") + } + + return nil +} + +func registerNativeFROSTSigningUnmarshallers(channel net.BroadcastChannel) { + channel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &nativeFROSTRoundOneCommitmentMessage{} + }) + channel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &nativeFROSTRoundTwoSignatureShareMessage{} + }) +} + +func executeNativeFROSTSigning( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, + engine NativeFROSTSigningEngine, + signerMaterial *nativeFROSTUniFFIV2SignerMaterial, +) (*frost.Signature, error) { + if engine == nil { + return nil, fmt.Errorf( + "%w: native FROST signing engine is unavailable", + ErrNativeCryptographyUnavailable, + ) + } + + if signerMaterial == nil { + return nil, fmt.Errorf( + "%w: native signer material is nil", + ErrNativeCryptographyUnavailable, + ) + } + + if err := signerMaterial.validate(); err != nil { + return nil, fmt.Errorf( + "%w: invalid native signer material: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + includedMembersSet, includedMembersIndexes, err := includedMembersFromRequest(request) + if err != nil { + return nil, err + } + + if _, ok := includedMembersSet[request.MemberIndex]; !ok { + return nil, fmt.Errorf( + "member [%v] not included in native FROST signing attempt", + request.MemberIndex, + ) + } + + messageBytes := request.Message.Bytes() + if len(messageBytes) == 0 { + messageBytes = []byte{0} + } + + ownNonces, ownCommitment, err := engine.GenerateNoncesAndCommitments( + signerMaterial.KeyPackage, + ) + if err != nil { + return nil, fmt.Errorf( + "native FROST round one generation failed: [%w]", + err, + ) + } + + if ownCommitment == nil { + return nil, fmt.Errorf("native FROST round one returned nil commitment") + } + + if ownCommitment.Identifier == "" { + return nil, fmt.Errorf("native FROST round one commitment identifier is empty") + } + + if len(ownCommitment.Data) == 0 { + return nil, fmt.Errorf("native FROST round one commitment data is empty") + } + + if ownNonces == nil { + return nil, fmt.Errorf("native FROST round one returned nil nonces") + } + + roundOneMessage := &nativeFROSTRoundOneCommitmentMessage{ + SenderIDValue: uint32(request.MemberIndex), + SessionIDValue: request.SessionID, + ParticipantIdentifier: ownCommitment.Identifier, + CommitmentData: append([]byte{}, ownCommitment.Data...), + } + + if err := request.Channel.Send( + ctx, + roundOneMessage, + net.BackoffRetransmissionStrategy, + ); err != nil { + return nil, fmt.Errorf("cannot send native FROST round one message: [%w]", err) + } + + roundOneMessages, err := collectNativeFROSTRoundOneMessages( + ctx, + request, + includedMembersSet, + includedMembersIndexes, + ) + if err != nil { + return nil, err + } + + commitmentsBySender := map[group.MemberIndex]*NativeFROSTCommitment{ + request.MemberIndex: ownCommitment, + } + + for senderID, message := range roundOneMessages { + commitmentsBySender[senderID] = &NativeFROSTCommitment{ + Identifier: message.ParticipantIdentifier, + Data: append([]byte{}, message.CommitmentData...), + } + } + + orderedCommitments := make([]*NativeFROSTCommitment, 0, len(includedMembersIndexes)) + for _, memberIndex := range includedMembersIndexes { + orderedCommitments = append( + orderedCommitments, + commitmentsBySender[memberIndex], + ) + } + + signingPackage, err := engine.NewSigningPackage( + messageBytes, + orderedCommitments, + ) + if err != nil { + return nil, fmt.Errorf( + "native FROST signing package creation failed: [%w]", + err, + ) + } + + if signingPackage == nil { + return nil, fmt.Errorf("native FROST signing package is nil") + } + + ownSignatureShare, err := engine.Sign( + signingPackage, + ownNonces, + signerMaterial.KeyPackage, + ) + if err != nil { + return nil, fmt.Errorf("native FROST round two signing failed: [%w]", err) + } + + if ownSignatureShare == nil { + return nil, fmt.Errorf("native FROST round two returned nil signature share") + } + + if ownSignatureShare.Identifier == "" { + return nil, fmt.Errorf("native FROST signature share identifier is empty") + } + + if len(ownSignatureShare.Data) == 0 { + return nil, fmt.Errorf("native FROST signature share data is empty") + } + + roundTwoMessage := &nativeFROSTRoundTwoSignatureShareMessage{ + SenderIDValue: uint32(request.MemberIndex), + SessionIDValue: request.SessionID, + ParticipantIdentifier: ownSignatureShare.Identifier, + SignatureShareData: append([]byte{}, ownSignatureShare.Data...), + } + + if err := request.Channel.Send( + ctx, + roundTwoMessage, + net.BackoffRetransmissionStrategy, + ); err != nil { + return nil, fmt.Errorf("cannot send native FROST round two message: [%w]", err) + } + + roundTwoMessages, err := collectNativeFROSTRoundTwoMessages( + ctx, + request, + includedMembersSet, + includedMembersIndexes, + ) + if err != nil { + return nil, err + } + + signatureSharesBySender := map[group.MemberIndex]*NativeFROSTSignatureShare{ + request.MemberIndex: ownSignatureShare, + } + + for senderID, message := range roundTwoMessages { + signatureSharesBySender[senderID] = &NativeFROSTSignatureShare{ + Identifier: message.ParticipantIdentifier, + Data: append([]byte{}, message.SignatureShareData...), + } + } + + orderedSignatureShares := make([]*NativeFROSTSignatureShare, 0, len(includedMembersIndexes)) + for _, memberIndex := range includedMembersIndexes { + orderedSignatureShares = append( + orderedSignatureShares, + signatureSharesBySender[memberIndex], + ) + } + + signatureBytes, err := engine.Aggregate( + signingPackage, + orderedSignatureShares, + signerMaterial.PublicKeyPackage, + ) + if err != nil { + return nil, fmt.Errorf("native FROST aggregation failed: [%w]", err) + } + + signature := &frost.Signature{} + if err := signature.Unmarshal(signatureBytes); err != nil { + return nil, fmt.Errorf( + "native FROST aggregation returned invalid signature: [%w]", + err, + ) + } + + if logger != nil { + logger.Debugf( + "[member:%v] native FROST signing completed with [%v] participants", + request.MemberIndex, + len(includedMembersIndexes), + ) + } + + return signature, nil +} + +func includedMembersFromRequest( + request *NativeExecutionFFISigningRequest, +) (map[group.MemberIndex]struct{}, []group.MemberIndex, error) { + if request == nil { + return nil, nil, fmt.Errorf("request is nil") + } + + if request.GroupSize <= 0 { + return nil, nil, fmt.Errorf("group size must be positive") + } + + includedMembersSet := make(map[group.MemberIndex]struct{}) + + if request.Attempt != nil && len(request.Attempt.IncludedMembersIndexes) > 0 { + for _, memberIndex := range request.Attempt.IncludedMembersIndexes { + if memberIndex == 0 { + return nil, nil, fmt.Errorf("included member index is zero") + } + + includedMembersSet[memberIndex] = struct{}{} + } + } else { + excludedMembersSet := make(map[group.MemberIndex]struct{}) + if request.Attempt != nil { + for _, memberIndex := range request.Attempt.ExcludedMembersIndexes { + if memberIndex == 0 { + continue + } + + excludedMembersSet[memberIndex] = struct{}{} + } + } + + for i := 1; i <= request.GroupSize; i++ { + memberIndex := group.MemberIndex(i) + if _, excluded := excludedMembersSet[memberIndex]; !excluded { + includedMembersSet[memberIndex] = struct{}{} + } + } + } + + if len(includedMembersSet) == 0 { + return nil, nil, fmt.Errorf("included members set is empty") + } + + includedMembersIndexes := make([]group.MemberIndex, 0, len(includedMembersSet)) + for memberIndex := range includedMembersSet { + includedMembersIndexes = append(includedMembersIndexes, memberIndex) + } + + sort.Slice(includedMembersIndexes, func(i, j int) bool { + return includedMembersIndexes[i] < includedMembersIndexes[j] + }) + + return includedMembersSet, includedMembersIndexes, nil +} + +func collectNativeFROSTRoundOneMessages( + ctx context.Context, + request *NativeExecutionFFISigningRequest, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, +) (map[group.MemberIndex]*nativeFROSTRoundOneCommitmentMessage, error) { + expectedMessagesCount := len(includedMembersIndexes) - 1 + if expectedMessagesCount <= 0 { + return map[group.MemberIndex]*nativeFROSTRoundOneCommitmentMessage{}, nil + } + + recvCtx, cancelRecvCtx := context.WithCancel(ctx) + defer cancelRecvCtx() + + messageChan := make(chan *nativeFROSTRoundOneCommitmentMessage, expectedMessagesCount*4+1) + + request.Channel.Recv(recvCtx, func(message net.Message) { + payload, ok := message.Payload().(*nativeFROSTRoundOneCommitmentMessage) + if !ok { + return + } + + if !shouldAcceptNativeFROSTMessage( + request, + includedMembersSet, + payload.SenderID(), + payload.SessionID(), + message.SenderPublicKey(), + ) { + return + } + + select { + case messageChan <- payload: + default: + } + }) + + receivedMessages := make(map[group.MemberIndex]*nativeFROSTRoundOneCommitmentMessage) + + for len(receivedMessages) < expectedMessagesCount { + select { + case <-ctx.Done(): + return nil, fmt.Errorf( + "native FROST round one collection interrupted: [%w]", + ctx.Err(), + ) + + case message := <-messageChan: + receivedMessages[message.SenderID()] = message + } + } + + return receivedMessages, nil +} + +func collectNativeFROSTRoundTwoMessages( + ctx context.Context, + request *NativeExecutionFFISigningRequest, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, +) (map[group.MemberIndex]*nativeFROSTRoundTwoSignatureShareMessage, error) { + expectedMessagesCount := len(includedMembersIndexes) - 1 + if expectedMessagesCount <= 0 { + return map[group.MemberIndex]*nativeFROSTRoundTwoSignatureShareMessage{}, nil + } + + recvCtx, cancelRecvCtx := context.WithCancel(ctx) + defer cancelRecvCtx() + + messageChan := make(chan *nativeFROSTRoundTwoSignatureShareMessage, expectedMessagesCount*4+1) + + request.Channel.Recv(recvCtx, func(message net.Message) { + payload, ok := message.Payload().(*nativeFROSTRoundTwoSignatureShareMessage) + if !ok { + return + } + + if !shouldAcceptNativeFROSTMessage( + request, + includedMembersSet, + payload.SenderID(), + payload.SessionID(), + message.SenderPublicKey(), + ) { + return + } + + select { + case messageChan <- payload: + default: + } + }) + + receivedMessages := make(map[group.MemberIndex]*nativeFROSTRoundTwoSignatureShareMessage) + + for len(receivedMessages) < expectedMessagesCount { + select { + case <-ctx.Done(): + return nil, fmt.Errorf( + "native FROST round two collection interrupted: [%w]", + ctx.Err(), + ) + + case message := <-messageChan: + receivedMessages[message.SenderID()] = message + } + } + + return receivedMessages, nil +} + +func shouldAcceptNativeFROSTMessage( + request *NativeExecutionFFISigningRequest, + includedMembersSet map[group.MemberIndex]struct{}, + senderID group.MemberIndex, + sessionID string, + senderPublicKey []byte, +) bool { + if senderID == 0 { + return false + } + + if senderID == request.MemberIndex { + return false + } + + if sessionID != request.SessionID { + return false + } + + if _, included := includedMembersSet[senderID]; !included { + return false + } + + if request.MembershipValidator == nil { + return true + } + + return request.MembershipValidator.IsValidMembership(senderID, senderPublicKey) +} diff --git a/pkg/frost/signing/native_frost_protocol_frost_native_test.go b/pkg/frost/signing/native_frost_protocol_frost_native_test.go new file mode 100644 index 0000000000..f9c5a3e6d6 --- /dev/null +++ b/pkg/frost/signing/native_frost_protocol_frost_native_test.go @@ -0,0 +1,329 @@ +//go:build frost_native + +package signing + +import ( + "context" + "crypto/sha256" + "crypto/sha512" + "encoding/json" + "errors" + "fmt" + "math/big" + "sort" + "sync" + "testing" + "time" + + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/net/local" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +type deterministicNativeFROSTSigningEngine struct{} + +func (dnfse *deterministicNativeFROSTSigningEngine) GenerateNoncesAndCommitments( + keyPackage *NativeFROSTKeyPackage, +) (*NativeFROSTNonces, *NativeFROSTCommitment, error) { + if keyPackage == nil { + return nil, nil, fmt.Errorf("key package is nil") + } + + if keyPackage.Identifier == "" { + return nil, nil, fmt.Errorf("key package identifier is empty") + } + + nonceSeed := sha256.Sum256( + append( + []byte("nonce:"), + []byte(keyPackage.Identifier)..., + ), + ) + commitmentSeed := sha256.Sum256( + append( + []byte("commitment:"), + []byte(keyPackage.Identifier)..., + ), + ) + + return &NativeFROSTNonces{ + Data: nonceSeed[:], + }, &NativeFROSTCommitment{ + Identifier: keyPackage.Identifier, + Data: commitmentSeed[:], + }, nil +} + +func (dnfse *deterministicNativeFROSTSigningEngine) NewSigningPackage( + message []byte, + commitments []*NativeFROSTCommitment, +) (*NativeFROSTSigningPackage, error) { + if len(commitments) == 0 { + return nil, fmt.Errorf("commitments are empty") + } + + serialized := append([]byte{}, message...) + for _, commitment := range commitments { + if commitment == nil { + return nil, fmt.Errorf("commitment is nil") + } + + serialized = append(serialized, []byte(commitment.Identifier)...) + serialized = append(serialized, commitment.Data...) + } + + packageDigest := sha256.Sum256(serialized) + + return &NativeFROSTSigningPackage{ + Data: packageDigest[:], + }, nil +} + +func (dnfse *deterministicNativeFROSTSigningEngine) Sign( + signingPackage *NativeFROSTSigningPackage, + nonces *NativeFROSTNonces, + keyPackage *NativeFROSTKeyPackage, +) (*NativeFROSTSignatureShare, error) { + if signingPackage == nil { + return nil, fmt.Errorf("signing package is nil") + } + + if nonces == nil { + return nil, fmt.Errorf("nonces are nil") + } + + if keyPackage == nil { + return nil, fmt.Errorf("key package is nil") + } + + serialized := append([]byte{}, signingPackage.Data...) + serialized = append(serialized, nonces.Data...) + serialized = append(serialized, []byte(keyPackage.Identifier)...) + serialized = append(serialized, keyPackage.Data...) + + shareDigest := sha256.Sum256(serialized) + + return &NativeFROSTSignatureShare{ + Identifier: keyPackage.Identifier, + Data: shareDigest[:], + }, nil +} + +func (dnfse *deterministicNativeFROSTSigningEngine) Aggregate( + signingPackage *NativeFROSTSigningPackage, + signatureShares []*NativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, +) ([]byte, error) { + if signingPackage == nil { + return nil, fmt.Errorf("signing package is nil") + } + + if publicKeyPackage == nil { + return nil, fmt.Errorf("public key package is nil") + } + + if len(signatureShares) == 0 { + return nil, fmt.Errorf("signature shares are empty") + } + + orderedSignatureShares := append([]*NativeFROSTSignatureShare{}, signatureShares...) + sort.Slice(orderedSignatureShares, func(i, j int) bool { + return orderedSignatureShares[i].Identifier < orderedSignatureShares[j].Identifier + }) + + serialized := append([]byte{}, signingPackage.Data...) + for _, signatureShare := range orderedSignatureShares { + if signatureShare == nil { + return nil, fmt.Errorf("signature share is nil") + } + + serialized = append(serialized, []byte(signatureShare.Identifier)...) + serialized = append(serialized, signatureShare.Data...) + } + + serialized = append(serialized, []byte(publicKeyPackage.VerifyingKey)...) + + signatureDigest := sha512.Sum512(serialized) + + return signatureDigest[:], nil +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_NativeFROSTPath( + t *testing.T, +) { + RegisterNativeFROSTSigningEngine(&deterministicNativeFROSTSigningEngine{}) + t.Cleanup(UnregisterNativeFROSTSigningEngine) + + provider := local.Connect() + channel, err := provider.BroadcastChannelFor("native-frost-signing-protocol-test") + if err != nil { + t.Fatalf("failed creating broadcast channel: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + primitive.RegisterUnmarshallers(channel) + + participantCount := 3 + includedMembers := []group.MemberIndex{1, 2, 3} + + requests := make([]*NativeExecutionFFISigningRequest, participantCount) + for i := 0; i < participantCount; i++ { + memberIndex := group.MemberIndex(i + 1) + requests[i], err = newNativeFROSTSigningRequestForTest( + memberIndex, + includedMembers, + channel, + participantCount, + ) + if err != nil { + t.Fatalf("failed preparing request for member [%v]: [%v]", memberIndex, err) + } + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelCtx() + + results := make([]*frostSignatureResultForTest, participantCount) + wg := sync.WaitGroup{} + wg.Add(participantCount) + + for i := 0; i < participantCount; i++ { + go func(index int) { + defer wg.Done() + + signature, signErr := primitive.Sign(ctx, nil, requests[index]) + results[index] = &frostSignatureResultForTest{ + signature: signature, + err: signErr, + } + }(i) + } + + wg.Wait() + + for i, result := range results { + if result == nil { + t.Fatalf("missing result for member [%v]", i+1) + } + + if result.err != nil { + t.Fatalf( + "unexpected signing error for member [%v]: [%v]", + i+1, + result.err, + ) + } + + if result.signature == nil { + t.Fatalf("nil signature for member [%v]", i+1) + } + } + + for i := 1; i < participantCount; i++ { + if !results[0].signature.Equals(results[i].signature) { + t.Fatalf( + "signature mismatch\nfirst: [%v]\nsecond: [%v]", + results[0].signature, + results[i].signature, + ) + } + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_NativeFROSTPathWithoutEngine( + t *testing.T, +) { + UnregisterNativeFROSTSigningEngine() + t.Cleanup(UnregisterNativeFROSTSigningEngine) + + provider := local.Connect() + channel, err := provider.BroadcastChannelFor("native-frost-signing-protocol-unavailable-test") + if err != nil { + t.Fatalf("failed creating broadcast channel: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + primitive.RegisterUnmarshallers(channel) + + request, err := newNativeFROSTSigningRequestForTest( + 1, + []group.MemberIndex{1}, + channel, + 1, + ) + if err != nil { + t.Fatalf("failed creating native request: [%v]", err) + } + + _, err = primitive.Sign(context.Background(), nil, request) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } +} + +type frostSignatureResultForTest struct { + signature *frost.Signature + err error +} + +func newNativeFROSTSigningRequestForTest( + memberIndex group.MemberIndex, + includedMembers []group.MemberIndex, + channel net.BroadcastChannel, + groupSize int, +) (*NativeExecutionFFISigningRequest, error) { + keyPackage := &NativeFROSTKeyPackage{ + Identifier: fmt.Sprintf("member-%v", memberIndex), + Data: []byte{ + byte(memberIndex), + 0x01, + }, + } + + verifyingShares := make(map[string]string) + for i := 1; i <= groupSize; i++ { + verifyingShares[fmt.Sprintf("member-%v", i)] = fmt.Sprintf("share-%v", i) + } + + payload, err := json.Marshal(&nativeFROSTUniFFIV2SignerMaterial{ + KeyPackage: keyPackage, + PublicKeyPackage: &NativeFROSTPublicKeyPackage{ + VerifyingShares: verifyingShares, + VerifyingKey: "verifying-key", + }, + }) + if err != nil { + return nil, err + } + + return &NativeExecutionFFISigningRequest{ + Message: bigOneForTest(), + SessionID: "native-frost-signing-session", + MemberIndex: memberIndex, + GroupSize: groupSize, + DishonestThreshold: 1, + Channel: channel, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV2, + Payload: payload, + }, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: includedMembers[0], + IncludedMembersIndexes: append([]group.MemberIndex{}, includedMembers...), + }, + }, nil +} + +func bigOneForTest() *big.Int { + return big.NewInt(1) +} From 753bf311a4e7ff2cfeacdf6f439c921276b8f540 Mon Sep 17 00:00:00 2001 From: maclane Date: Sat, 21 Feb 2026 09:28:06 -0600 Subject: [PATCH 35/96] tbtc: validate strict ffi path with v2 signer material --- ...igning_native_backend_frost_native_test.go | 133 ++++++++++++++++-- 1 file changed, 120 insertions(+), 13 deletions(-) diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index ed8c2dfec9..b267d4b206 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -3,10 +3,15 @@ package tbtc import ( + "bytes" "context" "crypto/ecdsa" + "encoding/json" "errors" + "fmt" "math/big" + "strconv" + "sync/atomic" "testing" "github.com/ipfs/go-log/v2" @@ -16,7 +21,22 @@ import ( ) type countingNativeExecutionFFISigningPrimitive struct { - signCalls int + signCalls atomic.Int64 +} + +type deterministicNativeExecutionFFISigningPrimitiveForTBTC struct { + signCalls atomic.Int64 +} + +var deterministicNativeFROSTSignatureForTBTC = [frost.SignatureSize]byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, } func (cnefsp *countingNativeExecutionFFISigningPrimitive) Sign( @@ -24,7 +44,7 @@ func (cnefsp *countingNativeExecutionFFISigningPrimitive) Sign( logger log.StandardLogger, request *frostsigning.NativeExecutionFFISigningRequest, ) (*frost.Signature, error) { - cnefsp.signCalls++ + cnefsp.signCalls.Add(1) return &frost.Signature{}, nil } @@ -33,6 +53,42 @@ func (cnefsp *countingNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( ) { } +func (dnefspf *deterministicNativeExecutionFFISigningPrimitiveForTBTC) Sign( + ctx context.Context, + logger log.StandardLogger, + request *frostsigning.NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + dnefspf.signCalls.Add(1) + + if request == nil { + return nil, fmt.Errorf("request is nil") + } + + nativeSignerMaterial := request.SignerMaterial + if nativeSignerMaterial == nil { + return nil, fmt.Errorf("native signer material is nil") + } + + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV2 { + return nil, fmt.Errorf( + "unexpected signer material format: [%s]", + nativeSignerMaterial.Format, + ) + } + + signature := &frost.Signature{} + if err := signature.Unmarshal(deterministicNativeFROSTSignatureForTBTC[:]); err != nil { + return nil, err + } + + return signature, nil +} + +func (dnefspf *deterministicNativeExecutionFFISigningPrimitiveForTBTC) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { +} + func TestConfigureFrostSigningBackend_FFIStrictConfigured_BuildAdapter(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() @@ -155,18 +211,25 @@ func TestSigningExecutor_Sign_FFIStrictBackend_WithNativeSignerMaterial( t *testing.T, ) { executor := setupSigningExecutor(t) + configureSignersWithNativeFROSTUniFFIV2Material(t, executor) + + primitive := &deterministicNativeExecutionFFISigningPrimitiveForTBTC{} frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() frostsigning.UnregisterNativeExecutionBridge() frostsigning.UnregisterNativeExecutionFFIExecutor() frostsigning.RegisterNativeExecutionAdapterForBuild() + err := frostsigning.RegisterNativeExecutionFFISigningPrimitive(primitive) + if err != nil { + t.Fatalf("unexpected native FFI primitive registration error: [%v]", err) + } t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) - err := configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + err = configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) if err != nil { t.Fatalf("unexpected strict ffi backend config error: [%v]", err) } @@ -190,14 +253,21 @@ func TestSigningExecutor_Sign_FFIStrictBackend_WithNativeSignerMaterial( t.Fatalf("unexpected strict ffi signing error: [%v]", err) } - walletPublicKey := executor.wallet().publicKey - if !ecdsa.Verify( - walletPublicKey, - message.Bytes(), - new(big.Int).SetBytes(signature.R[:]), - new(big.Int).SetBytes(signature.S[:]), - ) { - t.Fatalf("invalid signature: [%+v]", signature) + signatureBytes, err := signature.Marshal() + if err != nil { + t.Fatalf("cannot marshal signature: [%v]", err) + } + + if !bytes.Equal(signatureBytes, deterministicNativeFROSTSignatureForTBTC[:]) { + t.Fatalf( + "unexpected native FROST signature\nexpected: [%x]\nactual: [%x]", + deterministicNativeFROSTSignatureForTBTC[:], + signatureBytes, + ) + } + + if primitive.signCalls.Load() == 0 { + t.Fatal("expected native FFI primitive sign call") } if endBlock <= startBlock { @@ -256,11 +326,11 @@ func TestSigningExecutor_Sign_NativeBackend_FallsBackWhenOnlyLegacySignerMateria t.Fatalf("unexpected native backend signing error: [%v]", err) } - if primitive.signCalls != 0 { + if primitive.signCalls.Load() != 0 { t.Fatalf( "unexpected native primitive sign calls count\nexpected: [%d]\nactual: [%d]", 0, - primitive.signCalls, + primitive.signCalls.Load(), ) } @@ -278,3 +348,40 @@ func TestSigningExecutor_Sign_NativeBackend_FallsBackWhenOnlyLegacySignerMateria t.Fatal("wrong end block") } } + +func configureSignersWithNativeFROSTUniFFIV2Material( + t *testing.T, + executor *signingExecutor, +) { + t.Helper() + + publicKeyPackage := &frostsigning.NativeFROSTPublicKeyPackage{ + VerifyingShares: map[string]string{ + "1": "share-1", + }, + VerifyingKey: "group-verifying-key", + } + + for _, signer := range executor.signers { + keyPackage := &frostsigning.NativeFROSTKeyPackage{ + Identifier: strconv.FormatUint(uint64(signer.signingGroupMemberIndex), 10), + Data: []byte{byte(signer.signingGroupMemberIndex)}, + } + + payload, err := json.Marshal(struct { + KeyPackage *frostsigning.NativeFROSTKeyPackage `json:"keyPackage"` + PublicKeyPackage *frostsigning.NativeFROSTPublicKeyPackage `json:"publicKeyPackage"` + }{ + KeyPackage: keyPackage, + PublicKeyPackage: publicKeyPackage, + }) + if err != nil { + t.Fatalf("cannot marshal native signer material payload: [%v]", err) + } + + signer.signerMaterial = &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV2, + Payload: payload, + } + } +} From f765eece5d542dd09275615c86f15aa0f9090a44 Mon Sep 17 00:00:00 2001 From: maclane Date: Sat, 21 Feb 2026 15:27:17 -0600 Subject: [PATCH 36/96] Add build-tagged UniFFI native FROST signing engine scaffold --- go.mod | 3 + go.sum | 2 + ...ffi_primitive_transitional_frost_native.go | 4 + ...native_frost_engine_uniffi_frost_native.go | 230 ++++++++++++++++ ...e_frost_engine_uniffi_frost_native_test.go | 246 ++++++++++++++++++ ...niffi_registration_frost_native_default.go | 7 + ...uniffi_registration_frost_native_uniffi.go | 177 +++++++++++++ ...i_registration_frost_native_uniffi_test.go | 113 ++++++++ 8 files changed, 782 insertions(+) create mode 100644 pkg/frost/signing/native_frost_engine_uniffi_frost_native.go create mode 100644 pkg/frost/signing/native_frost_engine_uniffi_frost_native_test.go create mode 100644 pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go create mode 100644 pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go create mode 100644 pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go diff --git a/go.mod b/go.mod index 8e99078976..51c3460842 100644 --- a/go.mod +++ b/go.mod @@ -180,6 +180,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect + github.com/zecdev/frost-uniffi-sdk v0.0.0-20260221162625-51e08b3fb886 go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect @@ -202,3 +203,5 @@ require ( lukechampine.com/blake3 v1.2.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) + +replace github.com/zecdev/frost-uniffi-sdk => github.com/tlabs-xyz/frost-uniffi-sdk v0.0.0-20260221162625-51e08b3fb886 diff --git a/go.sum b/go.sum index 74807931a2..596f6af8e9 100644 --- a/go.sum +++ b/go.sum @@ -725,6 +725,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/tlabs-xyz/frost-uniffi-sdk v0.0.0-20260221162625-51e08b3fb886 h1:A4ZWyfNci/u+tnld6gtl419eBGtECIMPwIAKqsc6nQQ= +github.com/tlabs-xyz/frost-uniffi-sdk v0.0.0-20260221162625-51e08b3fb886/go.mod h1:90FbRr9Nyr8Zf3LRwGG8eISJJ1xhq4HXmkTMqAqsEz8= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 28eaa7dd08..dfc6f3cbeb 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -18,6 +18,10 @@ func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( NativeExecutionFFISigningPrimitive, error, ) { + if err := registerBuildTaggedNativeFROSTSigningEngine(); err != nil { + return nil, err + } + return &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{}, nil } diff --git a/pkg/frost/signing/native_frost_engine_uniffi_frost_native.go b/pkg/frost/signing/native_frost_engine_uniffi_frost_native.go new file mode 100644 index 0000000000..9c2cbd0b0e --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_uniffi_frost_native.go @@ -0,0 +1,230 @@ +//go:build frost_native + +package signing + +import "fmt" + +type uniFFINativeFROSTCommitment struct { + Identifier string + Data []byte +} + +type uniFFINativeFROSTSignatureShare struct { + Identifier string + Data []byte +} + +type uniFFINativeFROSTBridge interface { + GenerateNoncesAndCommitments( + keyPackageIdentifier string, + keyPackageData []byte, + ) (noncesData []byte, commitmentIdentifier string, commitmentData []byte, err error) + NewSigningPackage( + message []byte, + commitments []uniFFINativeFROSTCommitment, + ) (signingPackageData []byte, err error) + Sign( + signingPackageData []byte, + noncesData []byte, + keyPackageIdentifier string, + keyPackageData []byte, + ) (signatureShareIdentifier string, signatureShareData []byte, err error) + Aggregate( + signingPackageData []byte, + signatureShares []uniFFINativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, + ) (signature []byte, err error) +} + +type uniFFINativeFROSTSigningEngine struct { + bridge uniFFINativeFROSTBridge +} + +func newUniFFINativeFROSTSigningEngine( + bridge uniFFINativeFROSTBridge, +) (NativeFROSTSigningEngine, error) { + if bridge == nil { + return nil, fmt.Errorf("uniffi native FROST bridge is nil") + } + + return &uniFFINativeFROSTSigningEngine{ + bridge: bridge, + }, nil +} + +func (unfse *uniFFINativeFROSTSigningEngine) GenerateNoncesAndCommitments( + keyPackage *NativeFROSTKeyPackage, +) (*NativeFROSTNonces, *NativeFROSTCommitment, error) { + if keyPackage == nil { + return nil, nil, fmt.Errorf("key package is nil") + } + + if keyPackage.Identifier == "" { + return nil, nil, fmt.Errorf("key package identifier is empty") + } + + if len(keyPackage.Data) == 0 { + return nil, nil, fmt.Errorf("key package data is empty") + } + + noncesData, commitmentIdentifier, commitmentData, err := unfse.bridge.GenerateNoncesAndCommitments( + keyPackage.Identifier, + append([]byte{}, keyPackage.Data...), + ) + if err != nil { + return nil, nil, err + } + + return &NativeFROSTNonces{ + Data: append([]byte{}, noncesData...), + }, &NativeFROSTCommitment{ + Identifier: commitmentIdentifier, + Data: append([]byte{}, commitmentData...), + }, nil +} + +func (unfse *uniFFINativeFROSTSigningEngine) NewSigningPackage( + message []byte, + commitments []*NativeFROSTCommitment, +) (*NativeFROSTSigningPackage, error) { + if len(commitments) == 0 { + return nil, fmt.Errorf("commitments are empty") + } + + bridgeCommitments := make([]uniFFINativeFROSTCommitment, 0, len(commitments)) + for i, commitment := range commitments { + if commitment == nil { + return nil, fmt.Errorf("commitment [%d] is nil", i) + } + + if commitment.Identifier == "" { + return nil, fmt.Errorf("commitment [%d] identifier is empty", i) + } + + if len(commitment.Data) == 0 { + return nil, fmt.Errorf("commitment [%d] data is empty", i) + } + + bridgeCommitments = append(bridgeCommitments, uniFFINativeFROSTCommitment{ + Identifier: commitment.Identifier, + Data: append([]byte{}, commitment.Data...), + }) + } + + signingPackageData, err := unfse.bridge.NewSigningPackage( + append([]byte{}, message...), + bridgeCommitments, + ) + if err != nil { + return nil, err + } + + return &NativeFROSTSigningPackage{ + Data: append([]byte{}, signingPackageData...), + }, nil +} + +func (unfse *uniFFINativeFROSTSigningEngine) Sign( + signingPackage *NativeFROSTSigningPackage, + nonces *NativeFROSTNonces, + keyPackage *NativeFROSTKeyPackage, +) (*NativeFROSTSignatureShare, error) { + if signingPackage == nil { + return nil, fmt.Errorf("signing package is nil") + } + + if len(signingPackage.Data) == 0 { + return nil, fmt.Errorf("signing package data is empty") + } + + if nonces == nil { + return nil, fmt.Errorf("nonces are nil") + } + + if len(nonces.Data) == 0 { + return nil, fmt.Errorf("nonces data is empty") + } + + if keyPackage == nil { + return nil, fmt.Errorf("key package is nil") + } + + if keyPackage.Identifier == "" { + return nil, fmt.Errorf("key package identifier is empty") + } + + if len(keyPackage.Data) == 0 { + return nil, fmt.Errorf("key package data is empty") + } + + identifier, signatureShareData, err := unfse.bridge.Sign( + append([]byte{}, signingPackage.Data...), + append([]byte{}, nonces.Data...), + keyPackage.Identifier, + append([]byte{}, keyPackage.Data...), + ) + if err != nil { + return nil, err + } + + return &NativeFROSTSignatureShare{ + Identifier: identifier, + Data: append([]byte{}, signatureShareData...), + }, nil +} + +func (unfse *uniFFINativeFROSTSigningEngine) Aggregate( + signingPackage *NativeFROSTSigningPackage, + signatureShares []*NativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, +) ([]byte, error) { + if signingPackage == nil { + return nil, fmt.Errorf("signing package is nil") + } + + if len(signingPackage.Data) == 0 { + return nil, fmt.Errorf("signing package data is empty") + } + + if len(signatureShares) == 0 { + return nil, fmt.Errorf("signature shares are empty") + } + + if publicKeyPackage == nil { + return nil, fmt.Errorf("public key package is nil") + } + + bridgeSignatureShares := make([]uniFFINativeFROSTSignatureShare, 0, len(signatureShares)) + for i, signatureShare := range signatureShares { + if signatureShare == nil { + return nil, fmt.Errorf("signature share [%d] is nil", i) + } + + if signatureShare.Identifier == "" { + return nil, fmt.Errorf("signature share [%d] identifier is empty", i) + } + + if len(signatureShare.Data) == 0 { + return nil, fmt.Errorf("signature share [%d] data is empty", i) + } + + bridgeSignatureShares = append( + bridgeSignatureShares, + uniFFINativeFROSTSignatureShare{ + Identifier: signatureShare.Identifier, + Data: append([]byte{}, signatureShare.Data...), + }, + ) + } + + signature, err := unfse.bridge.Aggregate( + append([]byte{}, signingPackage.Data...), + bridgeSignatureShares, + publicKeyPackage, + ) + if err != nil { + return nil, err + } + + return append([]byte{}, signature...), nil +} diff --git a/pkg/frost/signing/native_frost_engine_uniffi_frost_native_test.go b/pkg/frost/signing/native_frost_engine_uniffi_frost_native_test.go new file mode 100644 index 0000000000..ba263706c6 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_uniffi_frost_native_test.go @@ -0,0 +1,246 @@ +//go:build frost_native + +package signing + +import ( + "bytes" + "errors" + "testing" +) + +type mockUniFFINativeFROSTBridge struct { + generateNoncesAndCommitmentsFn func( + keyPackageIdentifier string, + keyPackageData []byte, + ) ([]byte, string, []byte, error) + newSigningPackageFn func( + message []byte, + commitments []uniFFINativeFROSTCommitment, + ) ([]byte, error) + signFn func( + signingPackageData []byte, + noncesData []byte, + keyPackageIdentifier string, + keyPackageData []byte, + ) (string, []byte, error) + aggregateFn func( + signingPackageData []byte, + signatureShares []uniFFINativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, + ) ([]byte, error) +} + +func (munfsb *mockUniFFINativeFROSTBridge) GenerateNoncesAndCommitments( + keyPackageIdentifier string, + keyPackageData []byte, +) ([]byte, string, []byte, error) { + return munfsb.generateNoncesAndCommitmentsFn( + keyPackageIdentifier, + keyPackageData, + ) +} + +func (munfsb *mockUniFFINativeFROSTBridge) NewSigningPackage( + message []byte, + commitments []uniFFINativeFROSTCommitment, +) ([]byte, error) { + return munfsb.newSigningPackageFn(message, commitments) +} + +func (munfsb *mockUniFFINativeFROSTBridge) Sign( + signingPackageData []byte, + noncesData []byte, + keyPackageIdentifier string, + keyPackageData []byte, +) (string, []byte, error) { + return munfsb.signFn( + signingPackageData, + noncesData, + keyPackageIdentifier, + keyPackageData, + ) +} + +func (munfsb *mockUniFFINativeFROSTBridge) Aggregate( + signingPackageData []byte, + signatureShares []uniFFINativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, +) ([]byte, error) { + return munfsb.aggregateFn(signingPackageData, signatureShares, publicKeyPackage) +} + +func TestNewUniFFINativeFROSTSigningEngine_NilBridge(t *testing.T) { + _, err := newUniFFINativeFROSTSigningEngine(nil) + if err == nil { + t.Fatal("expected error") + } +} + +func TestUniFFINativeFROSTSigningEngine_GenerateNoncesAndCommitments(t *testing.T) { + var capturedIdentifier string + var capturedData []byte + + engine, err := newUniFFINativeFROSTSigningEngine(&mockUniFFINativeFROSTBridge{ + generateNoncesAndCommitmentsFn: func( + keyPackageIdentifier string, + keyPackageData []byte, + ) ([]byte, string, []byte, error) { + capturedIdentifier = keyPackageIdentifier + capturedData = append([]byte{}, keyPackageData...) + return []byte{0x01, 0x02}, "id-1", []byte{0x03, 0x04}, nil + }, + }) + if err != nil { + t.Fatalf("unexpected constructor error: [%v]", err) + } + + nonces, commitment, err := engine.GenerateNoncesAndCommitments( + &NativeFROSTKeyPackage{ + Identifier: "member-1", + Data: []byte{0xaa, 0xbb}, + }, + ) + if err != nil { + t.Fatalf("unexpected generation error: [%v]", err) + } + + if capturedIdentifier != "member-1" { + t.Fatalf( + "unexpected key package identifier\nexpected: [%v]\nactual: [%v]", + "member-1", + capturedIdentifier, + ) + } + + if !bytes.Equal(capturedData, []byte{0xaa, 0xbb}) { + t.Fatalf( + "unexpected key package data\nexpected: [%x]\nactual: [%x]", + []byte{0xaa, 0xbb}, + capturedData, + ) + } + + if !bytes.Equal(nonces.Data, []byte{0x01, 0x02}) { + t.Fatalf( + "unexpected nonces data\nexpected: [%x]\nactual: [%x]", + []byte{0x01, 0x02}, + nonces.Data, + ) + } + + if commitment.Identifier != "id-1" { + t.Fatalf( + "unexpected commitment identifier\nexpected: [%v]\nactual: [%v]", + "id-1", + commitment.Identifier, + ) + } + + if !bytes.Equal(commitment.Data, []byte{0x03, 0x04}) { + t.Fatalf( + "unexpected commitment data\nexpected: [%x]\nactual: [%x]", + []byte{0x03, 0x04}, + commitment.Data, + ) + } +} + +func TestUniFFINativeFROSTSigningEngine_SignAndAggregate(t *testing.T) { + expectedErr := errors.New("aggregate error") + + engine, err := newUniFFINativeFROSTSigningEngine(&mockUniFFINativeFROSTBridge{ + generateNoncesAndCommitmentsFn: func( + keyPackageIdentifier string, + keyPackageData []byte, + ) ([]byte, string, []byte, error) { + return nil, "", nil, nil + }, + newSigningPackageFn: func( + message []byte, + commitments []uniFFINativeFROSTCommitment, + ) ([]byte, error) { + return []byte{0x01}, nil + }, + signFn: func( + signingPackageData []byte, + noncesData []byte, + keyPackageIdentifier string, + keyPackageData []byte, + ) (string, []byte, error) { + return "member-1", []byte{0x99}, nil + }, + aggregateFn: func( + signingPackageData []byte, + signatureShares []uniFFINativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, + ) ([]byte, error) { + return nil, expectedErr + }, + }) + if err != nil { + t.Fatalf("unexpected constructor error: [%v]", err) + } + + signingPackage, err := engine.NewSigningPackage( + []byte{0xab}, + []*NativeFROSTCommitment{ + { + Identifier: "member-1", + Data: []byte{0x11}, + }, + }, + ) + if err != nil { + t.Fatalf("unexpected signing package error: [%v]", err) + } + + signatureShare, err := engine.Sign( + signingPackage, + &NativeFROSTNonces{ + Data: []byte{0x22}, + }, + &NativeFROSTKeyPackage{ + Identifier: "member-1", + Data: []byte{0x33}, + }, + ) + if err != nil { + t.Fatalf("unexpected sign error: [%v]", err) + } + + if signatureShare.Identifier != "member-1" { + t.Fatalf( + "unexpected signature share identifier\nexpected: [%v]\nactual: [%v]", + "member-1", + signatureShare.Identifier, + ) + } + + if !bytes.Equal(signatureShare.Data, []byte{0x99}) { + t.Fatalf( + "unexpected signature share data\nexpected: [%x]\nactual: [%x]", + []byte{0x99}, + signatureShare.Data, + ) + } + + _, err = engine.Aggregate( + signingPackage, + []*NativeFROSTSignatureShare{ + signatureShare, + }, + &NativeFROSTPublicKeyPackage{ + VerifyingShares: map[string]string{ + "member-1": "share-1", + }, + VerifyingKey: "pubkey", + }, + ) + if !errors.Is(err, expectedErr) { + t.Fatalf( + "unexpected aggregate error\nexpected: [%v]\nactual: [%v]", + expectedErr, + err, + ) + } +} diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go new file mode 100644 index 0000000000..673483a929 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go @@ -0,0 +1,7 @@ +//go:build frost_native && !(frost_uniffi_sdk && cgo) + +package signing + +func registerBuildTaggedNativeFROSTSigningEngine() error { + return nil +} diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go new file mode 100644 index 0000000000..6d7aa80051 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go @@ -0,0 +1,177 @@ +//go:build frost_native && frost_uniffi_sdk && cgo + +package signing + +import ( + "fmt" + + frostuniffi "github.com/zecdev/frost-uniffi-sdk/frost_go_ffi" +) + +type buildTaggedUniFFINativeFROSTBridge struct{} + +func registerBuildTaggedNativeFROSTSigningEngine() error { + engine, err := newUniFFINativeFROSTSigningEngine( + &buildTaggedUniFFINativeFROSTBridge{}, + ) + if err != nil { + return err + } + + return RegisterNativeFROSTSigningEngine(engine) +} + +func recoverUniFFIPanic(err *error) { + if r := recover(); r != nil { + *err = fmt.Errorf("uniffi panic: [%v]", r) + } +} + +func (btnufb *buildTaggedUniFFINativeFROSTBridge) GenerateNoncesAndCommitments( + keyPackageIdentifier string, + keyPackageData []byte, +) ( + noncesData []byte, + commitmentIdentifier string, + commitmentData []byte, + err error, +) { + defer recoverUniFFIPanic(&err) + + firstRoundCommitment, err := frostuniffi.GenerateNoncesAndCommitments( + frostuniffi.FrostKeyPackage{ + Identifier: frostuniffi.ParticipantIdentifier{ + Data: keyPackageIdentifier, + }, + Data: append([]byte{}, keyPackageData...), + }, + ) + if err != nil { + return nil, "", nil, fmt.Errorf( + "cannot generate nonces and commitments: [%w]", + err, + ) + } + + return append([]byte{}, firstRoundCommitment.Nonces.Data...), + firstRoundCommitment.Commitments.Identifier.Data, + append([]byte{}, firstRoundCommitment.Commitments.Data...), + nil +} + +func (btnufb *buildTaggedUniFFINativeFROSTBridge) NewSigningPackage( + message []byte, + commitments []uniFFINativeFROSTCommitment, +) (signingPackageData []byte, err error) { + defer recoverUniFFIPanic(&err) + + uniffiCommitments := make( + []frostuniffi.FrostSigningCommitments, + 0, + len(commitments), + ) + + for _, commitment := range commitments { + uniffiCommitments = append( + uniffiCommitments, + frostuniffi.FrostSigningCommitments{ + Identifier: frostuniffi.ParticipantIdentifier{ + Data: commitment.Identifier, + }, + Data: append([]byte{}, commitment.Data...), + }, + ) + } + + signingPackage, err := frostuniffi.NewSigningPackage( + frostuniffi.Message{ + Data: append([]byte{}, message...), + }, + uniffiCommitments, + ) + if err != nil { + return nil, fmt.Errorf("cannot build signing package: [%w]", err) + } + + return append([]byte{}, signingPackage.Data...), nil +} + +func (btnufb *buildTaggedUniFFINativeFROSTBridge) Sign( + signingPackageData []byte, + noncesData []byte, + keyPackageIdentifier string, + keyPackageData []byte, +) (signatureShareIdentifier string, signatureShareData []byte, err error) { + defer recoverUniFFIPanic(&err) + + signatureShare, err := frostuniffi.Sign( + frostuniffi.FrostSigningPackage{ + Data: append([]byte{}, signingPackageData...), + }, + frostuniffi.FrostSigningNonces{ + Data: append([]byte{}, noncesData...), + }, + frostuniffi.FrostKeyPackage{ + Identifier: frostuniffi.ParticipantIdentifier{ + Data: keyPackageIdentifier, + }, + Data: append([]byte{}, keyPackageData...), + }, + ) + if err != nil { + return "", nil, fmt.Errorf("cannot produce signature share: [%w]", err) + } + + return signatureShare.Identifier.Data, append([]byte{}, signatureShare.Data...), nil +} + +func (btnufb *buildTaggedUniFFINativeFROSTBridge) Aggregate( + signingPackageData []byte, + signatureShares []uniFFINativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, +) (signature []byte, err error) { + defer recoverUniFFIPanic(&err) + + uniffiSignatureShares := make( + []frostuniffi.FrostSignatureShare, + 0, + len(signatureShares), + ) + for _, signatureShare := range signatureShares { + uniffiSignatureShares = append( + uniffiSignatureShares, + frostuniffi.FrostSignatureShare{ + Identifier: frostuniffi.ParticipantIdentifier{ + Data: signatureShare.Identifier, + }, + Data: append([]byte{}, signatureShare.Data...), + }, + ) + } + + uniffiVerifyingShares := make( + map[frostuniffi.ParticipantIdentifier]string, + len(publicKeyPackage.VerifyingShares), + ) + for identifier, verifyingShare := range publicKeyPackage.VerifyingShares { + uniffiVerifyingShares[frostuniffi.ParticipantIdentifier{ + Data: identifier, + }] = verifyingShare + } + + resultSignature, err := frostuniffi.Aggregate( + frostuniffi.FrostSigningPackage{ + Data: append([]byte{}, signingPackageData...), + }, + uniffiSignatureShares, + frostuniffi.FrostPublicKeyPackage{ + VerifyingShares: uniffiVerifyingShares, + VerifyingKey: publicKeyPackage.VerifyingKey, + }, + ) + if err != nil { + return nil, fmt.Errorf("cannot aggregate signature shares: [%w]", err) + } + + return append([]byte{}, resultSignature.Data...), nil +} diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go new file mode 100644 index 0000000000..0f80fc3168 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go @@ -0,0 +1,113 @@ +//go:build frost_native && frost_uniffi_sdk && cgo + +package signing + +import ( + "testing" + + frostuniffi "github.com/zecdev/frost-uniffi-sdk/frost_go_ffi" +) + +func TestBuildTaggedUniFFINativeFROSTBridge_EndToEndSigning(t *testing.T) { + engine, err := newUniFFINativeFROSTSigningEngine( + &buildTaggedUniFFINativeFROSTBridge{}, + ) + if err != nil { + t.Fatalf("unexpected engine constructor error: [%v]", err) + } + + keygen, err := frostuniffi.TrustedDealerKeygenFrom( + frostuniffi.Configuration{ + MinSigners: 2, + MaxSigners: 2, + Secret: []byte{}, + }, + ) + if err != nil { + t.Fatalf("cannot generate trusted dealer key shares: [%v]", err) + } + + keyPackages := make([]*NativeFROSTKeyPackage, 0, len(keygen.SecretShares)) + for _, secretShare := range keygen.SecretShares { + keyPackage, err := frostuniffi.VerifyAndGetKeyPackageFrom(secretShare) + if err != nil { + t.Fatalf("cannot verify key package from secret share: [%v]", err) + } + + keyPackages = append( + keyPackages, + &NativeFROSTKeyPackage{ + Identifier: keyPackage.Identifier.Data, + Data: append([]byte{}, keyPackage.Data...), + }, + ) + } + + if len(keyPackages) != 2 { + t.Fatalf( + "unexpected key package count\nexpected: [%v]\nactual: [%v]", + 2, + len(keyPackages), + ) + } + + nonces := make([]*NativeFROSTNonces, 0, len(keyPackages)) + commitments := make([]*NativeFROSTCommitment, 0, len(keyPackages)) + for _, keyPackage := range keyPackages { + generatedNonces, generatedCommitment, err := engine.GenerateNoncesAndCommitments( + keyPackage, + ) + if err != nil { + t.Fatalf("cannot generate nonces and commitments: [%v]", err) + } + + nonces = append(nonces, generatedNonces) + commitments = append(commitments, generatedCommitment) + } + + message := []byte("keep-core uniffi bridge integration test") + signingPackage, err := engine.NewSigningPackage(message, commitments) + if err != nil { + t.Fatalf("cannot build signing package: [%v]", err) + } + + signatureShares := make([]*NativeFROSTSignatureShare, 0, len(keyPackages)) + for i, keyPackage := range keyPackages { + signatureShare, err := engine.Sign(signingPackage, nonces[i], keyPackage) + if err != nil { + t.Fatalf("cannot produce signature share: [%v]", err) + } + + signatureShares = append(signatureShares, signatureShare) + } + + verifyingShares := make(map[string]string, len(keygen.PublicKeyPackage.VerifyingShares)) + for identifier, verifyingShare := range keygen.PublicKeyPackage.VerifyingShares { + verifyingShares[identifier.Data] = verifyingShare + } + + signatureBytes, err := engine.Aggregate( + signingPackage, + signatureShares, + &NativeFROSTPublicKeyPackage{ + VerifyingShares: verifyingShares, + VerifyingKey: keygen.PublicKeyPackage.VerifyingKey, + }, + ) + if err != nil { + t.Fatalf("cannot aggregate signature shares: [%v]", err) + } + + err = frostuniffi.VerifySignature( + frostuniffi.Message{ + Data: message, + }, + frostuniffi.FrostSignature{ + Data: signatureBytes, + }, + keygen.PublicKeyPackage, + ) + if err != nil { + t.Fatalf("cannot verify aggregated signature: [%v]", err) + } +} From 90caa23f1f3fa0d96092ae8b5145a6866fd1ea84 Mon Sep 17 00:00:00 2001 From: maclane Date: Sat, 21 Feb 2026 16:45:05 -0600 Subject: [PATCH 37/96] tbtc: thread canonical wallet ID compatibility through chain models --- pkg/chain/ethereum/tbtc.go | 2 ++ pkg/tbtc/chain.go | 8 ++++++++ pkg/tbtc/chain_test.go | 4 ++++ pkg/tbtc/wallet_id.go | 12 ++++++++++++ pkg/tbtc/wallet_id_test.go | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 pkg/tbtc/wallet_id.go create mode 100644 pkg/tbtc/wallet_id_test.go diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index ec5c29d40f..99bb4a661c 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1397,6 +1397,7 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0) for _, event := range events { convertedEvent := &tbtc.NewWalletRegisteredEvent{ + WalletID: tbtc.DeriveLegacyWalletID(event.WalletPubKeyHash), EcdsaWalletID: event.EcdsaWalletID, WalletPublicKeyHash: event.WalletPubKeyHash, BlockNumber: event.Raw.BlockNumber, @@ -1474,6 +1475,7 @@ func (tc *TbtcChain) GetWallet( } return &tbtc.WalletChainData{ + WalletID: tbtc.DeriveLegacyWalletID(walletPublicKeyHash), EcdsaWalletID: wallet.EcdsaWalletID, MainUtxoHash: wallet.MainUtxoHash, PendingRedemptionsValue: wallet.PendingRedemptionsValue, diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 55206f86fb..f6d2a83238 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -329,6 +329,10 @@ type BridgeChain interface { // NewWalletRegisteredEvent represents a new wallet registered event. type NewWalletRegisteredEvent struct { + // WalletID is the canonical bridge wallet identifier. + // For legacy ECDSA wallets, this is derived as a left-padded + // 20-byte wallet public key hash. + WalletID [32]byte EcdsaWalletID [32]byte WalletPublicKeyHash [20]byte BlockNumber uint64 @@ -413,6 +417,10 @@ type DepositChainRequest struct { // WalletChainData represents wallet data stored on-chain. type WalletChainData struct { + // WalletID is the canonical bridge wallet identifier. + // For legacy ECDSA wallets, this is derived as a left-padded + // 20-byte wallet public key hash. + WalletID [32]byte EcdsaWalletID [32]byte MainUtxoHash [32]byte PendingRedemptionsValue uint64 diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 15bb4c94ca..d4850bf29a 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -916,6 +916,10 @@ func (lc *localChain) setWallet( lc.walletsMutex.Lock() defer lc.walletsMutex.Unlock() + if walletChainData != nil && walletChainData.WalletID == [32]byte{} { + walletChainData.WalletID = DeriveLegacyWalletID(walletPublicKeyHash) + } + lc.wallets[walletPublicKeyHash] = walletChainData } diff --git a/pkg/tbtc/wallet_id.go b/pkg/tbtc/wallet_id.go new file mode 100644 index 0000000000..e82177dea4 --- /dev/null +++ b/pkg/tbtc/wallet_id.go @@ -0,0 +1,12 @@ +package tbtc + +// DeriveLegacyWalletID derives the canonical bridge wallet ID for legacy +// ECDSA wallets from their 20-byte wallet public key hash. +// +// Legacy wallet ID format is a left-padded bytes20 hash: +// bytes32(uint256(uint160(walletPubKeyHash))). +func DeriveLegacyWalletID(walletPublicKeyHash [20]byte) [32]byte { + var walletID [32]byte + copy(walletID[12:], walletPublicKeyHash[:]) + return walletID +} diff --git a/pkg/tbtc/wallet_id_test.go b/pkg/tbtc/wallet_id_test.go new file mode 100644 index 0000000000..63577f8449 --- /dev/null +++ b/pkg/tbtc/wallet_id_test.go @@ -0,0 +1,37 @@ +package tbtc + +import ( + "encoding/hex" + "testing" +) + +func TestDeriveLegacyWalletID(t *testing.T) { + walletPublicKeyHashBytes, err := hex.DecodeString( + "e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + ) + if err != nil { + t.Fatalf("failed to decode wallet public key hash: [%v]", err) + } + + var walletPublicKeyHash [20]byte + copy(walletPublicKeyHash[:], walletPublicKeyHashBytes) + + expectedWalletIDBytes, err := hex.DecodeString( + "000000000000000000000000e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + ) + if err != nil { + t.Fatalf("failed to decode expected wallet ID: [%v]", err) + } + + var expectedWalletID [32]byte + copy(expectedWalletID[:], expectedWalletIDBytes) + + actualWalletID := DeriveLegacyWalletID(walletPublicKeyHash) + if actualWalletID != expectedWalletID { + t.Fatalf( + "unexpected wallet ID\nexpected: [%x]\nactual: [%x]", + expectedWalletID, + actualWalletID, + ) + } +} From a49e35d26c004bf4a483ec943701dc89a08465b3 Mon Sep 17 00:00:00 2001 From: maclane Date: Sat, 21 Feb 2026 17:48:20 -0600 Subject: [PATCH 38/96] tbtc: refresh bridge bindings and wire canonical wallet IDs --- pkg/chain/ethereum/tbtc.go | 72 +- pkg/chain/ethereum/tbtc/gen/_address/Bridge | 1 + pkg/chain/ethereum/tbtc/gen/abi/Bridge.go | 640 ++++++++- pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go | 323 +++++ .../ethereum/tbtc/gen/contract/Bridge.go | 1204 +++++++++++++++-- pkg/tbtc/chain.go | 5 + pkg/tbtc/chain_test.go | 19 + pkg/tbtc/node.go | 81 +- pkg/tbtc/wallet_id.go | 18 + pkg/tbtc/wallet_id_test.go | 52 + 10 files changed, 2314 insertions(+), 101 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 99bb4a661c..9ae4192b15 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1374,19 +1374,22 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( ) ([]*tbtc.NewWalletRegisteredEvent, error) { var startBlock uint64 var endBlock *uint64 + var walletID [][32]byte var ecdsaWalletID [][32]byte var walletPublicKeyHash [][20]byte if filter != nil { startBlock = filter.StartBlock endBlock = filter.EndBlock + walletID = filter.WalletID ecdsaWalletID = filter.EcdsaWalletID walletPublicKeyHash = filter.WalletPublicKeyHash } - events, err := tc.bridge.PastNewWalletRegisteredEvents( + v2Events, err := tc.bridge.PastNewWalletRegisteredV2Events( startBlock, endBlock, + walletID, ecdsaWalletID, walletPublicKeyHash, ) @@ -1394,10 +1397,10 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( return nil, err } - convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0) - for _, event := range events { + convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0, len(v2Events)) + for _, event := range v2Events { convertedEvent := &tbtc.NewWalletRegisteredEvent{ - WalletID: tbtc.DeriveLegacyWalletID(event.WalletPubKeyHash), + WalletID: event.WalletID, EcdsaWalletID: event.EcdsaWalletID, WalletPublicKeyHash: event.WalletPubKeyHash, BlockNumber: event.Raw.BlockNumber, @@ -1406,6 +1409,30 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( convertedEvents = append(convertedEvents, convertedEvent) } + // Fallback for legacy deployments that do not emit NewWalletRegisteredV2. + if len(convertedEvents) == 0 && len(walletID) == 0 { + legacyEvents, err := tc.bridge.PastNewWalletRegisteredEvents( + startBlock, + endBlock, + ecdsaWalletID, + walletPublicKeyHash, + ) + if err != nil { + return nil, err + } + + for _, event := range legacyEvents { + convertedEvent := &tbtc.NewWalletRegisteredEvent{ + WalletID: tbtc.DeriveLegacyWalletID(event.WalletPubKeyHash), + EcdsaWalletID: event.EcdsaWalletID, + WalletPublicKeyHash: event.WalletPubKeyHash, + BlockNumber: event.Raw.BlockNumber, + } + + convertedEvents = append(convertedEvents, convertedEvent) + } + } + sort.SliceStable( convertedEvents, func(i, j int) bool { @@ -1474,8 +1501,14 @@ func (tc *TbtcChain) GetWallet( return nil, fmt.Errorf("cannot parse wallet state: [%v]", err) } + walletID, err := tc.bridge.WalletID(walletPublicKeyHash) + if err != nil { + // Fallback for legacy deployments where walletID accessor may not exist. + walletID = tbtc.DeriveLegacyWalletID(walletPublicKeyHash) + } + return &tbtc.WalletChainData{ - WalletID: tbtc.DeriveLegacyWalletID(walletPublicKeyHash), + WalletID: walletID, EcdsaWalletID: wallet.EcdsaWalletID, MainUtxoHash: wallet.MainUtxoHash, PendingRedemptionsValue: wallet.PendingRedemptionsValue, @@ -1488,6 +1521,35 @@ func (tc *TbtcChain) GetWallet( }, nil } +func (tc *TbtcChain) WalletPublicKeyHashForWalletID( + walletID [32]byte, +) ([20]byte, error) { + walletPublicKeyHash, err := tc.bridge.WalletPubKeyHashForWalletID(walletID) + if err == nil { + if walletPublicKeyHash != [20]byte{} { + return walletPublicKeyHash, nil + } + } + + legacyWalletPublicKeyHash, ok := tbtc.WalletPublicKeyHashFromLegacyWalletID(walletID) + if ok { + return legacyWalletPublicKeyHash, nil + } + + if err != nil { + return [20]byte{}, fmt.Errorf( + "cannot resolve wallet public key hash for wallet ID [0x%x]: [%v]", + walletID, + err, + ) + } + + return [20]byte{}, fmt.Errorf( + "wallet public key hash not found for wallet ID [0x%x]", + walletID, + ) +} + func (tc *TbtcChain) OnWalletClosed( handler func(event *tbtc.WalletClosedEvent), ) subscription.EventSubscription { diff --git a/pkg/chain/ethereum/tbtc/gen/_address/Bridge b/pkg/chain/ethereum/tbtc/gen/_address/Bridge index e69de29bb2..7daa69d34b 100644 --- a/pkg/chain/ethereum/tbtc/gen/_address/Bridge +++ b/pkg/chain/ethereum/tbtc/gen/_address/Bridge @@ -0,0 +1 @@ +0x0000000000000000000000000000000000000000 diff --git a/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go b/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go index e76e6f779f..a862325a69 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go @@ -121,7 +121,7 @@ type WalletsWallet struct { // BridgeMetaData contains all meta data concerning the Bridge contract. var BridgeMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"name\":\"DepositParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"DepositRevealed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sweepTxHash\",\"type\":\"bytes32\"}],\"name\":\"DepositsSwept\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeDefeatTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeDefeated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"FraudParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"oldGovernance\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"GovernanceTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"}],\"name\":\"MovedFundsSweepTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sweepTxHash\",\"type\":\"bytes32\"}],\"name\":\"MovedFundsSwept\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsBelowDustReported\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"}],\"name\":\"MovingFundsCommitmentSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"}],\"name\":\"MovingFundsCompleted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"MovingFundsParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsTimeoutReset\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"NewWalletRegistered\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"NewWalletRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"RedemptionParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"}],\"name\":\"RedemptionRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"RedemptionTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"redemptionWatchtower\",\"type\":\"address\"}],\"name\":\"RedemptionWatchtowerSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"redemptionTxHash\",\"type\":\"bytes32\"}],\"name\":\"RedemptionsCompleted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spvMaintainer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"SpvMaintainerStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"TreasuryUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"VaultStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletClosed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletClosing\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletMovingFunds\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"name\":\"WalletParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletTerminated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletCreatedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletHeartbeatFailedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"activeWalletPubKeyHash\",\"outputs\":[{\"internalType\":\"bytes20\",\"name\":\"\",\"type\":\"bytes20\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"contractReferences\",\"outputs\":[{\"internalType\":\"contractBank\",\"name\":\"bank\",\"type\":\"address\"},{\"internalType\":\"contractIRelay\",\"name\":\"relay\",\"type\":\"address\"},{\"internalType\":\"contractIWalletRegistry\",\"name\":\"ecdsaWalletRegistry\",\"type\":\"address\"},{\"internalType\":\"contractReimbursementPool\",\"name\":\"reimbursementPool\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"preimage\",\"type\":\"bytes\"},{\"internalType\":\"bool\",\"name\":\"witness\",\"type\":\"bool\"}],\"name\":\"defeatFraudChallenge\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"heartbeatMessage\",\"type\":\"bytes\"}],\"name\":\"defeatFraudChallengeWithHeartbeat\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"depositParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"}],\"name\":\"deposits\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"revealedAt\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"sweptAt\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"internalType\":\"structDeposit.DepositRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"challengeKey\",\"type\":\"uint256\"}],\"name\":\"fraudChallenges\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"depositAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"reportedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"resolved\",\"type\":\"bool\"}],\"internalType\":\"structFraud.FraudChallenge\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"fraudParameters\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getRedemptionWatchtower\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"governance\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_bank\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_relay\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_treasury\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_ecdsaWalletRegistry\",\"type\":\"address\"},{\"internalType\":\"addresspayable\",\"name\":\"_reimbursementPool\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"_txProofDifficultyFactor\",\"type\":\"uint96\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"isVaultTrusted\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"liveWalletsCount\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"requestKey\",\"type\":\"uint256\"}],\"name\":\"movedFundsSweepRequests\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"enumMovingFunds.MovedFundsSweepRequestState\",\"name\":\"state\",\"type\":\"uint8\"}],\"internalType\":\"structMovingFunds.MovedFundsSweepRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"movingFundsParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes\",\"name\":\"preimageSha256\",\"type\":\"bytes\"}],\"name\":\"notifyFraudChallengeDefeatTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyMovedFundsSweepTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"}],\"name\":\"notifyMovingFundsBelowDust\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyMovingFundsTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"notifyRedemptionTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"notifyRedemptionVeto\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"notifyWalletCloseable\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"notifyWalletClosingPeriodElapsed\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"pendingRedemptions\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"}],\"internalType\":\"structRedemption.RedemptionRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"balanceOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"redemptionData\",\"type\":\"bytes\"}],\"name\":\"receiveBalanceApproval\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"redemptionParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"activeWalletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"requestNewWallet\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"}],\"name\":\"requestRedemption\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"resetMovingFundsTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.DepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"}],\"name\":\"revealDeposit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.DepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"name\":\"revealDepositWithExtraData\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"redemptionWatchtower\",\"type\":\"address\"}],\"name\":\"setRedemptionWatchtower\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spvMaintainer\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"setSpvMaintainerStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"setVaultStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"utxoKey\",\"type\":\"uint256\"}],\"name\":\"spentMainUTXOs\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"sweepTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"sweepProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"submitDepositSweepProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"preimageSha256\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"}],\"internalType\":\"structBitcoinTx.RSVSignature\",\"name\":\"signature\",\"type\":\"tuple\"}],\"name\":\"submitFraudChallenge\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"sweepTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"sweepProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"}],\"name\":\"submitMovedFundsSweepProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"uint256\",\"name\":\"walletMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"}],\"name\":\"submitMovingFundsCommitment\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"movingFundsTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"movingFundsProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"submitMovingFundsProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"redemptionTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"redemptionProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"submitRedemptionProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"timedOutRedemptions\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"}],\"internalType\":\"structRedemption.RedemptionRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"transferGovernance\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasury\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"txProofDifficultyFactor\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"name\":\"updateDepositParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateFraudParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateMovingFundsParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateRedemptionParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"updateTreasury\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"name\":\"updateWalletParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"walletParameters\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"wallets\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"mainUtxoHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint64\",\"name\":\"pendingRedemptionsValue\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsRequestedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"closingStartedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"pendingMovedFundsSweepRequestsCount\",\"type\":\"uint32\"},{\"internalType\":\"enumWallets.WalletState\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTargetWalletsCommitmentHash\",\"type\":\"bytes32\"}],\"internalType\":\"structWallets.Wallet\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"name\":\"DepositParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"DepositRevealed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newVault\",\"type\":\"address\"}],\"name\":\"DepositVaultFixed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sweepTxHash\",\"type\":\"bytes32\"}],\"name\":\"DepositsSwept\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeDefeatTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeDefeated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sighash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"}],\"name\":\"FraudChallengeSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"FraudParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"oldGovernance\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"GovernanceTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"}],\"name\":\"MovedFundsSweepTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"sweepTxHash\",\"type\":\"bytes32\"}],\"name\":\"MovedFundsSwept\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsBelowDustReported\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"submitter\",\"type\":\"address\"}],\"name\":\"MovingFundsCommitmentSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"}],\"name\":\"MovingFundsCompleted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"MovingFundsParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"MovingFundsTimeoutReset\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"NewWalletRegistered\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"NewWalletRegisteredV2\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"NewWalletRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"rebateStaking\",\"type\":\"address\"}],\"name\":\"RebateStakingSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"RedemptionParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"}],\"name\":\"RedemptionRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"RedemptionTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"redemptionWatchtower\",\"type\":\"address\"}],\"name\":\"RedemptionWatchtowerSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"redemptionTxHash\",\"type\":\"bytes32\"}],\"name\":\"RedemptionsCompleted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"spvMaintainer\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"SpvMaintainerStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"TreasuryUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"VaultStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletClosed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletClosing\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletMovingFunds\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"name\":\"WalletParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"WalletTerminated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletCreatedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletHeartbeatFailedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"activeWalletID\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"activeWalletPubKeyHash\",\"outputs\":[{\"internalType\":\"bytes20\",\"name\":\"\",\"type\":\"bytes20\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"contractReferences\",\"outputs\":[{\"internalType\":\"contractBank\",\"name\":\"bank\",\"type\":\"address\"},{\"internalType\":\"contractIRelay\",\"name\":\"relay\",\"type\":\"address\"},{\"internalType\":\"contractIWalletRegistry\",\"name\":\"ecdsaWalletRegistry\",\"type\":\"address\"},{\"internalType\":\"contractReimbursementPool\",\"name\":\"reimbursementPool\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"preimage\",\"type\":\"bytes\"},{\"internalType\":\"bool\",\"name\":\"witness\",\"type\":\"bool\"}],\"name\":\"defeatFraudChallenge\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"heartbeatMessage\",\"type\":\"bytes\"}],\"name\":\"defeatFraudChallengeWithHeartbeat\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"depositParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"depositKey\",\"type\":\"uint256\"}],\"name\":\"deposits\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"depositor\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"revealedAt\",\"type\":\"uint32\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"sweptAt\",\"type\":\"uint32\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"internalType\":\"structDeposit.DepositRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"challengeKey\",\"type\":\"uint256\"}],\"name\":\"fraudChallenges\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"depositAmount\",\"type\":\"uint256\"},{\"internalType\":\"uint32\",\"name\":\"reportedAt\",\"type\":\"uint32\"},{\"internalType\":\"bool\",\"name\":\"resolved\",\"type\":\"bool\"}],\"internalType\":\"structFraud.FraudChallenge\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"fraudParameters\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getRebateStaking\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getRedemptionWatchtower\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"governance\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_bank\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_relay\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_treasury\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_ecdsaWalletRegistry\",\"type\":\"address\"},{\"internalType\":\"addresspayable\",\"name\":\"_reimbursementPool\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"_txProofDifficultyFactor\",\"type\":\"uint96\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"initializeV2_FixVaultZeroDeposit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"isVaultTrusted\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"liveWalletsCount\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"requestKey\",\"type\":\"uint256\"}],\"name\":\"movedFundsSweepRequests\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"enumMovingFunds.MovedFundsSweepRequestState\",\"name\":\"state\",\"type\":\"uint8\"}],\"internalType\":\"structMovingFunds.MovedFundsSweepRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"movingFundsParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes\",\"name\":\"preimageSha256\",\"type\":\"bytes\"}],\"name\":\"notifyFraudChallengeDefeatTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyMovedFundsSweepTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"}],\"name\":\"notifyMovingFundsBelowDust\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"notifyMovingFundsTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"notifyRedemptionTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"}],\"name\":\"notifyRedemptionVeto\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"notifyWalletCloseable\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"notifyWalletClosingPeriodElapsed\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"pendingRedemptions\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"}],\"internalType\":\"structRedemption.RedemptionRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"balanceOwner\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"redemptionData\",\"type\":\"bytes\"}],\"name\":\"receiveBalanceApproval\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"redemptionParameters\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"activeWalletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"requestNewWallet\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes\",\"name\":\"redeemerOutputScript\",\"type\":\"bytes\"},{\"internalType\":\"uint64\",\"name\":\"amount\",\"type\":\"uint64\"}],\"name\":\"requestRedemption\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"resetMovingFundsTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.DepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"}],\"name\":\"revealDeposit\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"fundingTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.DepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"name\":\"revealDepositWithExtraData\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"rebateStaking\",\"type\":\"address\"}],\"name\":\"setRebateStaking\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"redemptionWatchtower\",\"type\":\"address\"}],\"name\":\"setRedemptionWatchtower\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"spvMaintainer\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"setSpvMaintainerStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"isTrusted\",\"type\":\"bool\"}],\"name\":\"setVaultStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"utxoKey\",\"type\":\"uint256\"}],\"name\":\"spentMainUTXOs\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"sweepTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"sweepProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"submitDepositSweepProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"walletPublicKey\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"preimageSha256\",\"type\":\"bytes\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"r\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"s\",\"type\":\"bytes32\"},{\"internalType\":\"uint8\",\"name\":\"v\",\"type\":\"uint8\"}],\"internalType\":\"structBitcoinTx.RSVSignature\",\"name\":\"signature\",\"type\":\"tuple\"}],\"name\":\"submitFraudChallenge\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"sweepTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"sweepProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"}],\"name\":\"submitMovedFundsSweepProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"walletMainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"uint256\",\"name\":\"walletMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"}],\"name\":\"submitMovingFundsCommitment\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"movingFundsTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"movingFundsProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"submitMovingFundsProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes4\",\"name\":\"version\",\"type\":\"bytes4\"},{\"internalType\":\"bytes\",\"name\":\"inputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes\",\"name\":\"outputVector\",\"type\":\"bytes\"},{\"internalType\":\"bytes4\",\"name\":\"locktime\",\"type\":\"bytes4\"}],\"internalType\":\"structBitcoinTx.Info\",\"name\":\"redemptionTx\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes\",\"name\":\"merkleProof\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"txIndexInBlock\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"bitcoinHeaders\",\"type\":\"bytes\"},{\"internalType\":\"bytes32\",\"name\":\"coinbasePreimage\",\"type\":\"bytes32\"},{\"internalType\":\"bytes\",\"name\":\"coinbaseProof\",\"type\":\"bytes\"}],\"internalType\":\"structBitcoinTx.Proof\",\"name\":\"redemptionProof\",\"type\":\"tuple\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"txHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"txOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"txOutputValue\",\"type\":\"uint64\"}],\"internalType\":\"structBitcoinTx.UTXO\",\"name\":\"mainUtxo\",\"type\":\"tuple\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"submitRedemptionProof\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"redemptionKey\",\"type\":\"uint256\"}],\"name\":\"timedOutRedemptions\",\"outputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"redeemer\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"requestedAmount\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"treasuryFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"txMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"requestedAt\",\"type\":\"uint32\"}],\"internalType\":\"structRedemption.RedemptionRequest\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"transferGovernance\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"treasury\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"txProofDifficultyFactor\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"depositDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"depositTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"depositRevealAheadPeriod\",\"type\":\"uint32\"}],\"name\":\"updateDepositParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint96\",\"name\":\"fraudChallengeDepositAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudChallengeDefeatTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"fraudSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"fraudNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateFraudParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"movingFundsTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"movingFundsDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutResetDelay\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movingFundsTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"},{\"internalType\":\"uint16\",\"name\":\"movingFundsCommitmentGasOffset\",\"type\":\"uint16\"},{\"internalType\":\"uint64\",\"name\":\"movedFundsSweepTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"movedFundsSweepTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"movedFundsSweepTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateMovingFundsParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"redemptionDustThreshold\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTreasuryFeeDivisor\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxFee\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"redemptionTxMaxTotalFee\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeout\",\"type\":\"uint32\"},{\"internalType\":\"uint96\",\"name\":\"redemptionTimeoutSlashingAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint32\",\"name\":\"redemptionTimeoutNotifierRewardMultiplier\",\"type\":\"uint32\"}],\"name\":\"updateRedemptionParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"treasury\",\"type\":\"address\"}],\"name\":\"updateTreasury\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"name\":\"updateWalletParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"walletID\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"walletParameters\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"walletCreationPeriod\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletCreationMaxBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"walletClosureMinBtcBalance\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletMaxAge\",\"type\":\"uint32\"},{\"internalType\":\"uint64\",\"name\":\"walletMaxBtcTransfer\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"walletClosingPeriod\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"walletId\",\"type\":\"bytes32\"}],\"name\":\"walletPubKeyHashForWalletID\",\"outputs\":[{\"internalType\":\"bytes20\",\"name\":\"\",\"type\":\"bytes20\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"wallets\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"mainUtxoHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint64\",\"name\":\"pendingRedemptionsValue\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsRequestedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"closingStartedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"pendingMovedFundsSweepRequestsCount\",\"type\":\"uint32\"},{\"internalType\":\"enumWallets.WalletState\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTargetWalletsCommitmentHash\",\"type\":\"bytes32\"}],\"internalType\":\"structWallets.Wallet\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"walletId\",\"type\":\"bytes32\"}],\"name\":\"walletsByWalletID\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"ecdsaWalletID\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"mainUtxoHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint64\",\"name\":\"pendingRedemptionsValue\",\"type\":\"uint64\"},{\"internalType\":\"uint32\",\"name\":\"createdAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsRequestedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"closingStartedAt\",\"type\":\"uint32\"},{\"internalType\":\"uint32\",\"name\":\"pendingMovedFundsSweepRequestsCount\",\"type\":\"uint32\"},{\"internalType\":\"enumWallets.WalletState\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTargetWalletsCommitmentHash\",\"type\":\"bytes32\"}],\"internalType\":\"structWallets.Wallet\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", } // BridgeABI is the input ABI used to generate the binding from. @@ -270,6 +270,37 @@ func (_Bridge *BridgeTransactorRaw) Transact(opts *bind.TransactOpts, method str return _Bridge.Contract.contract.Transact(opts, method, params...) } +// ActiveWalletID is a free data retrieval call binding the contract method 0x160c1730. +// +// Solidity: function activeWalletID() view returns(bytes32) +func (_Bridge *BridgeCaller) ActiveWalletID(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "activeWalletID") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// ActiveWalletID is a free data retrieval call binding the contract method 0x160c1730. +// +// Solidity: function activeWalletID() view returns(bytes32) +func (_Bridge *BridgeSession) ActiveWalletID() ([32]byte, error) { + return _Bridge.Contract.ActiveWalletID(&_Bridge.CallOpts) +} + +// ActiveWalletID is a free data retrieval call binding the contract method 0x160c1730. +// +// Solidity: function activeWalletID() view returns(bytes32) +func (_Bridge *BridgeCallerSession) ActiveWalletID() ([32]byte, error) { + return _Bridge.Contract.ActiveWalletID(&_Bridge.CallOpts) +} + // ActiveWalletPubKeyHash is a free data retrieval call binding the contract method 0xded1d24a. // // Solidity: function activeWalletPubKeyHash() view returns(bytes20) @@ -528,6 +559,37 @@ func (_Bridge *BridgeCallerSession) FraudParameters() (struct { return _Bridge.Contract.FraudParameters(&_Bridge.CallOpts) } +// GetRebateStaking is a free data retrieval call binding the contract method 0x3edf8238. +// +// Solidity: function getRebateStaking() view returns(address) +func (_Bridge *BridgeCaller) GetRebateStaking(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "getRebateStaking") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetRebateStaking is a free data retrieval call binding the contract method 0x3edf8238. +// +// Solidity: function getRebateStaking() view returns(address) +func (_Bridge *BridgeSession) GetRebateStaking() (common.Address, error) { + return _Bridge.Contract.GetRebateStaking(&_Bridge.CallOpts) +} + +// GetRebateStaking is a free data retrieval call binding the contract method 0x3edf8238. +// +// Solidity: function getRebateStaking() view returns(address) +func (_Bridge *BridgeCallerSession) GetRebateStaking() (common.Address, error) { + return _Bridge.Contract.GetRebateStaking(&_Bridge.CallOpts) +} + // GetRedemptionWatchtower is a free data retrieval call binding the contract method 0x5f3281ca. // // Solidity: function getRedemptionWatchtower() view returns(address) @@ -998,6 +1060,37 @@ func (_Bridge *BridgeCallerSession) TxProofDifficultyFactor() (*big.Int, error) return _Bridge.Contract.TxProofDifficultyFactor(&_Bridge.CallOpts) } +// WalletID is a free data retrieval call binding the contract method 0x858c14bd. +// +// Solidity: function walletID(bytes20 walletPubKeyHash) pure returns(bytes32) +func (_Bridge *BridgeCaller) WalletID(opts *bind.CallOpts, walletPubKeyHash [20]byte) ([32]byte, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "walletID", walletPubKeyHash) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// WalletID is a free data retrieval call binding the contract method 0x858c14bd. +// +// Solidity: function walletID(bytes20 walletPubKeyHash) pure returns(bytes32) +func (_Bridge *BridgeSession) WalletID(walletPubKeyHash [20]byte) ([32]byte, error) { + return _Bridge.Contract.WalletID(&_Bridge.CallOpts, walletPubKeyHash) +} + +// WalletID is a free data retrieval call binding the contract method 0x858c14bd. +// +// Solidity: function walletID(bytes20 walletPubKeyHash) pure returns(bytes32) +func (_Bridge *BridgeCallerSession) WalletID(walletPubKeyHash [20]byte) ([32]byte, error) { + return _Bridge.Contract.WalletID(&_Bridge.CallOpts, walletPubKeyHash) +} + // WalletParameters is a free data retrieval call binding the contract method 0x61ccf97a. // // Solidity: function walletParameters() view returns(uint32 walletCreationPeriod, uint64 walletCreationMinBtcBalance, uint64 walletCreationMaxBtcBalance, uint64 walletClosureMinBtcBalance, uint32 walletMaxAge, uint64 walletMaxBtcTransfer, uint32 walletClosingPeriod) @@ -1068,6 +1161,37 @@ func (_Bridge *BridgeCallerSession) WalletParameters() (struct { return _Bridge.Contract.WalletParameters(&_Bridge.CallOpts) } +// WalletPubKeyHashForWalletID is a free data retrieval call binding the contract method 0x9a4f2ea9. +// +// Solidity: function walletPubKeyHashForWalletID(bytes32 walletId) view returns(bytes20) +func (_Bridge *BridgeCaller) WalletPubKeyHashForWalletID(opts *bind.CallOpts, walletId [32]byte) ([20]byte, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "walletPubKeyHashForWalletID", walletId) + + if err != nil { + return *new([20]byte), err + } + + out0 := *abi.ConvertType(out[0], new([20]byte)).(*[20]byte) + + return out0, err + +} + +// WalletPubKeyHashForWalletID is a free data retrieval call binding the contract method 0x9a4f2ea9. +// +// Solidity: function walletPubKeyHashForWalletID(bytes32 walletId) view returns(bytes20) +func (_Bridge *BridgeSession) WalletPubKeyHashForWalletID(walletId [32]byte) ([20]byte, error) { + return _Bridge.Contract.WalletPubKeyHashForWalletID(&_Bridge.CallOpts, walletId) +} + +// WalletPubKeyHashForWalletID is a free data retrieval call binding the contract method 0x9a4f2ea9. +// +// Solidity: function walletPubKeyHashForWalletID(bytes32 walletId) view returns(bytes20) +func (_Bridge *BridgeCallerSession) WalletPubKeyHashForWalletID(walletId [32]byte) ([20]byte, error) { + return _Bridge.Contract.WalletPubKeyHashForWalletID(&_Bridge.CallOpts, walletId) +} + // Wallets is a free data retrieval call binding the contract method 0xe65e19d5. // // Solidity: function wallets(bytes20 walletPubKeyHash) view returns((bytes32,bytes32,uint64,uint32,uint32,uint32,uint32,uint8,bytes32)) @@ -1099,6 +1223,37 @@ func (_Bridge *BridgeCallerSession) Wallets(walletPubKeyHash [20]byte) (WalletsW return _Bridge.Contract.Wallets(&_Bridge.CallOpts, walletPubKeyHash) } +// WalletsByWalletID is a free data retrieval call binding the contract method 0xa9b2f9a3. +// +// Solidity: function walletsByWalletID(bytes32 walletId) view returns((bytes32,bytes32,uint64,uint32,uint32,uint32,uint32,uint8,bytes32)) +func (_Bridge *BridgeCaller) WalletsByWalletID(opts *bind.CallOpts, walletId [32]byte) (WalletsWallet, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "walletsByWalletID", walletId) + + if err != nil { + return *new(WalletsWallet), err + } + + out0 := *abi.ConvertType(out[0], new(WalletsWallet)).(*WalletsWallet) + + return out0, err + +} + +// WalletsByWalletID is a free data retrieval call binding the contract method 0xa9b2f9a3. +// +// Solidity: function walletsByWalletID(bytes32 walletId) view returns((bytes32,bytes32,uint64,uint32,uint32,uint32,uint32,uint8,bytes32)) +func (_Bridge *BridgeSession) WalletsByWalletID(walletId [32]byte) (WalletsWallet, error) { + return _Bridge.Contract.WalletsByWalletID(&_Bridge.CallOpts, walletId) +} + +// WalletsByWalletID is a free data retrieval call binding the contract method 0xa9b2f9a3. +// +// Solidity: function walletsByWalletID(bytes32 walletId) view returns((bytes32,bytes32,uint64,uint32,uint32,uint32,uint32,uint8,bytes32)) +func (_Bridge *BridgeCallerSession) WalletsByWalletID(walletId [32]byte) (WalletsWallet, error) { + return _Bridge.Contract.WalletsByWalletID(&_Bridge.CallOpts, walletId) +} + // EcdsaWalletCreatedCallback is a paid mutator transaction binding the contract method 0xa8fa0f42. // // Solidity: function __ecdsaWalletCreatedCallback(bytes32 ecdsaWalletID, bytes32 publicKeyX, bytes32 publicKeyY) returns() @@ -1204,6 +1359,27 @@ func (_Bridge *BridgeTransactorSession) Initialize(_bank common.Address, _relay return _Bridge.Contract.Initialize(&_Bridge.TransactOpts, _bank, _relay, _treasury, _ecdsaWalletRegistry, _reimbursementPool, _txProofDifficultyFactor) } +// InitializeV2FixVaultZeroDeposit is a paid mutator transaction binding the contract method 0x456ffee0. +// +// Solidity: function initializeV2_FixVaultZeroDeposit() returns() +func (_Bridge *BridgeTransactor) InitializeV2FixVaultZeroDeposit(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "initializeV2_FixVaultZeroDeposit") +} + +// InitializeV2FixVaultZeroDeposit is a paid mutator transaction binding the contract method 0x456ffee0. +// +// Solidity: function initializeV2_FixVaultZeroDeposit() returns() +func (_Bridge *BridgeSession) InitializeV2FixVaultZeroDeposit() (*types.Transaction, error) { + return _Bridge.Contract.InitializeV2FixVaultZeroDeposit(&_Bridge.TransactOpts) +} + +// InitializeV2FixVaultZeroDeposit is a paid mutator transaction binding the contract method 0x456ffee0. +// +// Solidity: function initializeV2_FixVaultZeroDeposit() returns() +func (_Bridge *BridgeTransactorSession) InitializeV2FixVaultZeroDeposit() (*types.Transaction, error) { + return _Bridge.Contract.InitializeV2FixVaultZeroDeposit(&_Bridge.TransactOpts) +} + // NotifyFraudChallengeDefeatTimeout is a paid mutator transaction binding the contract method 0x79fc4eb3. // // Solidity: function notifyFraudChallengeDefeatTimeout(bytes walletPublicKey, uint32[] walletMembersIDs, bytes preimageSha256) returns() @@ -1498,6 +1674,27 @@ func (_Bridge *BridgeTransactorSession) RevealDepositWithExtraData(fundingTx Bit return _Bridge.Contract.RevealDepositWithExtraData(&_Bridge.TransactOpts, fundingTx, reveal, extraData) } +// SetRebateStaking is a paid mutator transaction binding the contract method 0xca73c462. +// +// Solidity: function setRebateStaking(address rebateStaking) returns() +func (_Bridge *BridgeTransactor) SetRebateStaking(opts *bind.TransactOpts, rebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "setRebateStaking", rebateStaking) +} + +// SetRebateStaking is a paid mutator transaction binding the contract method 0xca73c462. +// +// Solidity: function setRebateStaking(address rebateStaking) returns() +func (_Bridge *BridgeSession) SetRebateStaking(rebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetRebateStaking(&_Bridge.TransactOpts, rebateStaking) +} + +// SetRebateStaking is a paid mutator transaction binding the contract method 0xca73c462. +// +// Solidity: function setRebateStaking(address rebateStaking) returns() +func (_Bridge *BridgeTransactorSession) SetRebateStaking(rebateStaking common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetRebateStaking(&_Bridge.TransactOpts, rebateStaking) +} + // SetRedemptionWatchtower is a paid mutator transaction binding the contract method 0xbe26ebad. // // Solidity: function setRedemptionWatchtower(address redemptionWatchtower) returns() @@ -2133,6 +2330,151 @@ func (_Bridge *BridgeFilterer) ParseDepositRevealed(log types.Log) (*BridgeDepos return event, nil } +// BridgeDepositVaultFixedIterator is returned from FilterDepositVaultFixed and is used to iterate over the raw logs and unpacked data for DepositVaultFixed events raised by the Bridge contract. +type BridgeDepositVaultFixedIterator struct { + Event *BridgeDepositVaultFixed // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BridgeDepositVaultFixedIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BridgeDepositVaultFixed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BridgeDepositVaultFixed) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BridgeDepositVaultFixedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeDepositVaultFixedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeDepositVaultFixed represents a DepositVaultFixed event raised by the Bridge contract. +type BridgeDepositVaultFixed struct { + DepositKey *big.Int + NewVault common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDepositVaultFixed is a free log retrieval operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. +// +// Solidity: event DepositVaultFixed(uint256 indexed depositKey, address newVault) +func (_Bridge *BridgeFilterer) FilterDepositVaultFixed(opts *bind.FilterOpts, depositKey []*big.Int) (*BridgeDepositVaultFixedIterator, error) { + + var depositKeyRule []interface{} + for _, depositKeyItem := range depositKey { + depositKeyRule = append(depositKeyRule, depositKeyItem) + } + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "DepositVaultFixed", depositKeyRule) + if err != nil { + return nil, err + } + return &BridgeDepositVaultFixedIterator{contract: _Bridge.contract, event: "DepositVaultFixed", logs: logs, sub: sub}, nil +} + +// WatchDepositVaultFixed is a free log subscription operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. +// +// Solidity: event DepositVaultFixed(uint256 indexed depositKey, address newVault) +func (_Bridge *BridgeFilterer) WatchDepositVaultFixed(opts *bind.WatchOpts, sink chan<- *BridgeDepositVaultFixed, depositKey []*big.Int) (event.Subscription, error) { + + var depositKeyRule []interface{} + for _, depositKeyItem := range depositKey { + depositKeyRule = append(depositKeyRule, depositKeyItem) + } + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "DepositVaultFixed", depositKeyRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BridgeDepositVaultFixed) + if err := _Bridge.contract.UnpackLog(event, "DepositVaultFixed", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseDepositVaultFixed is a log parse operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. +// +// Solidity: event DepositVaultFixed(uint256 indexed depositKey, address newVault) +func (_Bridge *BridgeFilterer) ParseDepositVaultFixed(log types.Log) (*BridgeDepositVaultFixed, error) { + event := new(BridgeDepositVaultFixed) + if err := _Bridge.contract.UnpackLog(event, "DepositVaultFixed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // BridgeDepositsSweptIterator is returned from FilterDepositsSwept and is used to iterate over the raw logs and unpacked data for DepositsSwept events raised by the Bridge contract. type BridgeDepositsSweptIterator struct { Event *BridgeDepositsSwept // Event containing the contract specifics and raw log @@ -4423,6 +4765,168 @@ func (_Bridge *BridgeFilterer) ParseNewWalletRegistered(log types.Log) (*BridgeN return event, nil } +// BridgeNewWalletRegisteredV2Iterator is returned from FilterNewWalletRegisteredV2 and is used to iterate over the raw logs and unpacked data for NewWalletRegisteredV2 events raised by the Bridge contract. +type BridgeNewWalletRegisteredV2Iterator struct { + Event *BridgeNewWalletRegisteredV2 // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BridgeNewWalletRegisteredV2Iterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BridgeNewWalletRegisteredV2) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BridgeNewWalletRegisteredV2) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BridgeNewWalletRegisteredV2Iterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeNewWalletRegisteredV2Iterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeNewWalletRegisteredV2 represents a NewWalletRegisteredV2 event raised by the Bridge contract. +type BridgeNewWalletRegisteredV2 struct { + WalletID [32]byte + EcdsaWalletID [32]byte + WalletPubKeyHash [20]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNewWalletRegisteredV2 is a free log retrieval operation binding the contract event 0x6a501a1d441e1c8b5490e52589d0d27d35504cf1063a8c848fef40f326710d4b. +// +// Solidity: event NewWalletRegisteredV2(bytes32 indexed walletID, bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) FilterNewWalletRegisteredV2(opts *bind.FilterOpts, walletID [][32]byte, ecdsaWalletID [][32]byte, walletPubKeyHash [][20]byte) (*BridgeNewWalletRegisteredV2Iterator, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + var ecdsaWalletIDRule []interface{} + for _, ecdsaWalletIDItem := range ecdsaWalletID { + ecdsaWalletIDRule = append(ecdsaWalletIDRule, ecdsaWalletIDItem) + } + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "NewWalletRegisteredV2", walletIDRule, ecdsaWalletIDRule, walletPubKeyHashRule) + if err != nil { + return nil, err + } + return &BridgeNewWalletRegisteredV2Iterator{contract: _Bridge.contract, event: "NewWalletRegisteredV2", logs: logs, sub: sub}, nil +} + +// WatchNewWalletRegisteredV2 is a free log subscription operation binding the contract event 0x6a501a1d441e1c8b5490e52589d0d27d35504cf1063a8c848fef40f326710d4b. +// +// Solidity: event NewWalletRegisteredV2(bytes32 indexed walletID, bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) WatchNewWalletRegisteredV2(opts *bind.WatchOpts, sink chan<- *BridgeNewWalletRegisteredV2, walletID [][32]byte, ecdsaWalletID [][32]byte, walletPubKeyHash [][20]byte) (event.Subscription, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + var ecdsaWalletIDRule []interface{} + for _, ecdsaWalletIDItem := range ecdsaWalletID { + ecdsaWalletIDRule = append(ecdsaWalletIDRule, ecdsaWalletIDItem) + } + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "NewWalletRegisteredV2", walletIDRule, ecdsaWalletIDRule, walletPubKeyHashRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BridgeNewWalletRegisteredV2) + if err := _Bridge.contract.UnpackLog(event, "NewWalletRegisteredV2", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseNewWalletRegisteredV2 is a log parse operation binding the contract event 0x6a501a1d441e1c8b5490e52589d0d27d35504cf1063a8c848fef40f326710d4b. +// +// Solidity: event NewWalletRegisteredV2(bytes32 indexed walletID, bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) ParseNewWalletRegisteredV2(log types.Log) (*BridgeNewWalletRegisteredV2, error) { + event := new(BridgeNewWalletRegisteredV2) + if err := _Bridge.contract.UnpackLog(event, "NewWalletRegisteredV2", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // BridgeNewWalletRequestedIterator is returned from FilterNewWalletRequested and is used to iterate over the raw logs and unpacked data for NewWalletRequested events raised by the Bridge contract. type BridgeNewWalletRequestedIterator struct { Event *BridgeNewWalletRequested // Event containing the contract specifics and raw log @@ -4556,6 +5060,140 @@ func (_Bridge *BridgeFilterer) ParseNewWalletRequested(log types.Log) (*BridgeNe return event, nil } +// BridgeRebateStakingSetIterator is returned from FilterRebateStakingSet and is used to iterate over the raw logs and unpacked data for RebateStakingSet events raised by the Bridge contract. +type BridgeRebateStakingSetIterator struct { + Event *BridgeRebateStakingSet // Event containing the contract specifics and raw log + + contract *bind.BoundContract // Generic contract to use for unpacking event data + event string // Event name to use for unpacking event data + + logs chan types.Log // Log channel receiving the found contract events + sub ethereum.Subscription // Subscription for errors, completion and termination + done bool // Whether the subscription completed delivering logs + fail error // Occurred error to stop iteration +} + +// Next advances the iterator to the subsequent event, returning whether there +// are any more events found. In case of a retrieval or parsing error, false is +// returned and Error() can be queried for the exact failure. +func (it *BridgeRebateStakingSetIterator) Next() bool { + // If the iterator failed, stop iterating + if it.fail != nil { + return false + } + // If the iterator completed, deliver directly whatever's available + if it.done { + select { + case log := <-it.logs: + it.Event = new(BridgeRebateStakingSet) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + // Iterator still in progress, wait for either a data or an error event + select { + case log := <-it.logs: + it.Event = new(BridgeRebateStakingSet) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +// Error returns any retrieval or parsing error occurred during filtering. +func (it *BridgeRebateStakingSetIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeRebateStakingSetIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeRebateStakingSet represents a RebateStakingSet event raised by the Bridge contract. +type BridgeRebateStakingSet struct { + RebateStaking common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRebateStakingSet is a free log retrieval operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. +// +// Solidity: event RebateStakingSet(address rebateStaking) +func (_Bridge *BridgeFilterer) FilterRebateStakingSet(opts *bind.FilterOpts) (*BridgeRebateStakingSetIterator, error) { + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "RebateStakingSet") + if err != nil { + return nil, err + } + return &BridgeRebateStakingSetIterator{contract: _Bridge.contract, event: "RebateStakingSet", logs: logs, sub: sub}, nil +} + +// WatchRebateStakingSet is a free log subscription operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. +// +// Solidity: event RebateStakingSet(address rebateStaking) +func (_Bridge *BridgeFilterer) WatchRebateStakingSet(opts *bind.WatchOpts, sink chan<- *BridgeRebateStakingSet) (event.Subscription, error) { + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "RebateStakingSet") + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + // New log arrived, parse the event and forward to the user + event := new(BridgeRebateStakingSet) + if err := _Bridge.contract.UnpackLog(event, "RebateStakingSet", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +// ParseRebateStakingSet is a log parse operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. +// +// Solidity: event RebateStakingSet(address rebateStaking) +func (_Bridge *BridgeFilterer) ParseRebateStakingSet(log types.Log) (*BridgeRebateStakingSet, error) { + event := new(BridgeRebateStakingSet) + if err := _Bridge.contract.UnpackLog(event, "RebateStakingSet", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // BridgeRedemptionParametersUpdatedIterator is returned from FilterRedemptionParametersUpdated and is used to iterate over the raw logs and unpacked data for RedemptionParametersUpdated events raised by the Bridge contract. type BridgeRedemptionParametersUpdatedIterator struct { Event *BridgeRedemptionParametersUpdated // Event containing the contract specifics and raw log diff --git a/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go b/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go index f7a5944669..5af214163a 100644 --- a/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go @@ -52,12 +52,14 @@ func init() { } BridgeCommand.AddCommand( + bActiveWalletIDCommand(), bActiveWalletPubKeyHashCommand(), bContractReferencesCommand(), bDepositParametersCommand(), bDepositsCommand(), bFraudChallengesCommand(), bFraudParametersCommand(), + bGetRebateStakingCommand(), bGetRedemptionWatchtowerCommand(), bGovernanceCommand(), bIsVaultTrustedCommand(), @@ -70,13 +72,17 @@ func init() { bTimedOutRedemptionsCommand(), bTreasuryCommand(), bTxProofDifficultyFactorCommand(), + bWalletIDCommand(), bWalletParametersCommand(), + bWalletPubKeyHashForWalletIDCommand(), bWalletsCommand(), + bWalletsByWalletIDCommand(), bDefeatFraudChallengeCommand(), bDefeatFraudChallengeWithHeartbeatCommand(), bEcdsaWalletCreatedCallbackCommand(), bEcdsaWalletHeartbeatFailedCallbackCommand(), bInitializeCommand(), + bInitializeV2FixVaultZeroDepositCommand(), bNotifyMovingFundsBelowDustCommand(), bNotifyRedemptionVetoCommand(), bNotifyWalletCloseableCommand(), @@ -87,6 +93,7 @@ func init() { bResetMovingFundsTimeoutCommand(), bRevealDepositCommand(), bRevealDepositWithExtraDataCommand(), + bSetRebateStakingCommand(), bSetRedemptionWatchtowerCommand(), bSetSpvMaintainerStatusCommand(), bSetVaultStatusCommand(), @@ -109,6 +116,40 @@ func init() { /// ------------------- Const methods ------------------- +func bActiveWalletIDCommand() *cobra.Command { + c := &cobra.Command{ + Use: "active-wallet-i-d", + Short: "Calls the view method activeWalletID on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bActiveWalletID, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bActiveWalletID(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + result, err := contract.ActiveWalletIDAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func bActiveWalletPubKeyHashCommand() *cobra.Command { c := &cobra.Command{ Use: "active-wallet-pub-key-hash", @@ -331,6 +372,40 @@ func bFraudParameters(c *cobra.Command, args []string) error { return nil } +func bGetRebateStakingCommand() *cobra.Command { + c := &cobra.Command{ + Use: "get-rebate-staking", + Short: "Calls the view method getRebateStaking on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bGetRebateStaking, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bGetRebateStaking(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + result, err := contract.GetRebateStakingAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func bGetRedemptionWatchtowerCommand() *cobra.Command { c := &cobra.Command{ Use: "get-redemption-watchtower", @@ -784,6 +859,49 @@ func bTxProofDifficultyFactor(c *cobra.Command, args []string) error { return nil } +func bWalletIDCommand() *cobra.Command { + c := &cobra.Command{ + Use: "wallet-i-d [arg_walletPubKeyHash]", + Short: "Calls the pure method walletID on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bWalletID, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bWalletID(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_walletPubKeyHash, err := decode.ParseBytes20(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_walletPubKeyHash, a bytes20, from passed value %v", + args[0], + ) + } + + result, err := contract.WalletIDAtBlock( + arg_walletPubKeyHash, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func bWalletParametersCommand() *cobra.Command { c := &cobra.Command{ Use: "wallet-parameters", @@ -818,6 +936,49 @@ func bWalletParameters(c *cobra.Command, args []string) error { return nil } +func bWalletPubKeyHashForWalletIDCommand() *cobra.Command { + c := &cobra.Command{ + Use: "wallet-pub-key-hash-for-wallet-i-d [arg_walletId]", + Short: "Calls the view method walletPubKeyHashForWalletID on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bWalletPubKeyHashForWalletID, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bWalletPubKeyHashForWalletID(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_walletId, err := decode.ParseBytes32(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_walletId, a bytes32, from passed value %v", + args[0], + ) + } + + result, err := contract.WalletPubKeyHashForWalletIDAtBlock( + arg_walletId, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func bWalletsCommand() *cobra.Command { c := &cobra.Command{ Use: "wallets [arg_walletPubKeyHash]", @@ -861,6 +1022,49 @@ func bWallets(c *cobra.Command, args []string) error { return nil } +func bWalletsByWalletIDCommand() *cobra.Command { + c := &cobra.Command{ + Use: "wallets-by-wallet-i-d [arg_walletId]", + Short: "Calls the view method walletsByWalletID on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bWalletsByWalletID, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bWalletsByWalletID(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_walletId, err := decode.ParseBytes32(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_walletId, a bytes32, from passed value %v", + args[0], + ) + } + + result, err := contract.WalletsByWalletIDAtBlock( + arg_walletId, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + /// ------------------- Non-const methods ------------------- func bDefeatFraudChallengeCommand() *cobra.Command { @@ -1296,6 +1500,60 @@ func bInitialize(c *cobra.Command, args []string) error { return nil } +func bInitializeV2FixVaultZeroDepositCommand() *cobra.Command { + c := &cobra.Command{ + Use: "initialize-v2-fix-vault-zero-deposit", + Short: "Calls the nonpayable method initializeV2FixVaultZeroDeposit on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bInitializeV2FixVaultZeroDeposit, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bInitializeV2FixVaultZeroDeposit(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.InitializeV2FixVaultZeroDeposit() + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallInitializeV2FixVaultZeroDeposit( + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + func bNotifyMovingFundsBelowDustCommand() *cobra.Command { c := &cobra.Command{ Use: "notify-moving-funds-below-dust [arg_walletPubKeyHash] [arg_mainUtxo_json]", @@ -2026,6 +2284,71 @@ func bRevealDepositWithExtraData(c *cobra.Command, args []string) error { return nil } +func bSetRebateStakingCommand() *cobra.Command { + c := &cobra.Command{ + Use: "set-rebate-staking [arg_rebateStaking]", + Short: "Calls the nonpayable method setRebateStaking on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bSetRebateStaking, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSetRebateStaking(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_rebateStaking, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_rebateStaking, a address, from passed value %v", + args[0], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.SetRebateStaking( + arg_rebateStaking, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSetRebateStaking( + arg_rebateStaking, + cmd.BlockFlagValue.Int, + ) + if err != nil { + return err + } + + cmd.PrintOutput("success") + + cmd.PrintOutput( + "the transaction was not submitted to the chain; " + + "please add the `--submit` flag", + ) + } + + return nil +} + func bSetRedemptionWatchtowerCommand() *cobra.Command { c := &cobra.Command{ Use: "set-redemption-watchtower [arg_redemptionWatchtower]", diff --git a/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go b/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go index ae73c92607..c0b3348064 100644 --- a/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go @@ -914,6 +914,130 @@ func (b *Bridge) InitializeGasEstimate( return result, err } +// Transaction submission. +func (b *Bridge) InitializeV2FixVaultZeroDeposit( + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction initializeV2FixVaultZeroDeposit", + ) + + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := b.contract.InitializeV2FixVaultZeroDeposit( + transactorOptions, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "initializeV2FixVaultZeroDeposit", + ) + } + + bLogger.Infof( + "submitted transaction initializeV2FixVaultZeroDeposit with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go b.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := b.contract.InitializeV2FixVaultZeroDeposit( + newTransactorOptions, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "initializeV2FixVaultZeroDeposit", + ) + } + + bLogger.Infof( + "submitted transaction initializeV2FixVaultZeroDeposit with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + b.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallInitializeV2FixVaultZeroDeposit( + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "initializeV2FixVaultZeroDeposit", + &result, + ) + + return err +} + +func (b *Bridge) InitializeV2FixVaultZeroDepositGasEstimate() (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "initializeV2FixVaultZeroDeposit", + b.contractABI, + b.transactor, + ) + + return result, err +} + // Transaction submission. func (b *Bridge) NotifyFraudChallengeDefeatTimeout( arg_walletPublicKey []byte, @@ -3026,6 +3150,144 @@ func (b *Bridge) RevealDepositWithExtraDataGasEstimate( return result, err } +// Transaction submission. +func (b *Bridge) SetRebateStaking( + arg_rebateStaking common.Address, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction setRebateStaking", + " params: ", + fmt.Sprint( + arg_rebateStaking, + ), + ) + + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() + + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions + + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", + ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) + } + + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := b.contract.SetRebateStaking( + transactorOptions, + arg_rebateStaking, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "setRebateStaking", + arg_rebateStaking, + ) + } + + bLogger.Infof( + "submitted transaction setRebateStaking with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + go b.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + // If original transactor options has a non-zero gas limit, that + // means the client code set it on their own. In that case, we + // should rewrite the gas limit from the original transaction + // for each resubmission. If the gas limit is not set by the client + // code, let the the submitter re-estimate the gas limit on each + // resubmission. + if transactorOptions.GasLimit != 0 { + newTransactorOptions.GasLimit = transactorOptions.GasLimit + } + + transaction, err := b.contract.SetRebateStaking( + newTransactorOptions, + arg_rebateStaking, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "setRebateStaking", + arg_rebateStaking, + ) + } + + bLogger.Infof( + "submitted transaction setRebateStaking with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + b.nonceManager.IncrementNonce() + + return transaction, err +} + +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallSetRebateStaking( + arg_rebateStaking common.Address, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "setRebateStaking", + &result, + arg_rebateStaking, + ) + + return err +} + +func (b *Bridge) SetRebateStakingGasEstimate( + arg_rebateStaking common.Address, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "setRebateStaking", + b.contractABI, + b.transactor, + arg_rebateStaking, + ) + + return result, err +} + // Transaction submission. func (b *Bridge) SetRedemptionWatchtower( arg_redemptionWatchtower common.Address, @@ -5706,8 +5968,8 @@ func (b *Bridge) UpdateWalletParametersGasEstimate( // ----- Const Methods ------ -func (b *Bridge) ActiveWalletPubKeyHash() ([20]byte, error) { - result, err := b.contract.ActiveWalletPubKeyHash( +func (b *Bridge) ActiveWalletID() ([32]byte, error) { + result, err := b.contract.ActiveWalletID( b.callerOptions, ) @@ -5716,17 +5978,17 @@ func (b *Bridge) ActiveWalletPubKeyHash() ([20]byte, error) { err, b.callerOptions.From, nil, - "activeWalletPubKeyHash", + "activeWalletID", ) } return result, err } -func (b *Bridge) ActiveWalletPubKeyHashAtBlock( +func (b *Bridge) ActiveWalletIDAtBlock( blockNumber *big.Int, -) ([20]byte, error) { - var result [20]byte +) ([32]byte, error) { + var result [32]byte err := chainutil.CallAtBlock( b.callerOptions.From, @@ -5736,22 +5998,15 @@ func (b *Bridge) ActiveWalletPubKeyHashAtBlock( b.caller, b.errorResolver, b.contractAddress, - "activeWalletPubKeyHash", + "activeWalletID", &result, ) return result, err } -type contractReferences struct { - Bank common.Address - Relay common.Address - EcdsaWalletRegistry common.Address - ReimbursementPool common.Address -} - -func (b *Bridge) ContractReferences() (contractReferences, error) { - result, err := b.contract.ContractReferences( +func (b *Bridge) ActiveWalletPubKeyHash() ([20]byte, error) { + result, err := b.contract.ActiveWalletPubKeyHash( b.callerOptions, ) @@ -5760,17 +6015,17 @@ func (b *Bridge) ContractReferences() (contractReferences, error) { err, b.callerOptions.From, nil, - "contractReferences", + "activeWalletPubKeyHash", ) } return result, err } -func (b *Bridge) ContractReferencesAtBlock( +func (b *Bridge) ActiveWalletPubKeyHashAtBlock( blockNumber *big.Int, -) (contractReferences, error) { - var result contractReferences +) ([20]byte, error) { + var result [20]byte err := chainutil.CallAtBlock( b.callerOptions.From, @@ -5780,7 +6035,51 @@ func (b *Bridge) ContractReferencesAtBlock( b.caller, b.errorResolver, b.contractAddress, - "contractReferences", + "activeWalletPubKeyHash", + &result, + ) + + return result, err +} + +type contractReferences struct { + Bank common.Address + Relay common.Address + EcdsaWalletRegistry common.Address + ReimbursementPool common.Address +} + +func (b *Bridge) ContractReferences() (contractReferences, error) { + result, err := b.contract.ContractReferences( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "contractReferences", + ) + } + + return result, err +} + +func (b *Bridge) ContractReferencesAtBlock( + blockNumber *big.Int, +) (contractReferences, error) { + var result contractReferences + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "contractReferences", &result, ) @@ -5961,6 +6260,43 @@ func (b *Bridge) FraudParametersAtBlock( return result, err } +func (b *Bridge) GetRebateStaking() (common.Address, error) { + result, err := b.contract.GetRebateStaking( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "getRebateStaking", + ) + } + + return result, err +} + +func (b *Bridge) GetRebateStakingAtBlock( + blockNumber *big.Int, +) (common.Address, error) { + var result common.Address + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "getRebateStaking", + &result, + ) + + return result, err +} + func (b *Bridge) GetRedemptionWatchtower() (common.Address, error) { result, err := b.contract.GetRedemptionWatchtower( b.callerOptions, @@ -6459,6 +6795,49 @@ func (b *Bridge) TxProofDifficultyFactorAtBlock( return result, err } +func (b *Bridge) WalletID( + arg_walletPubKeyHash [20]byte, +) ([32]byte, error) { + result, err := b.contract.WalletID( + b.callerOptions, + arg_walletPubKeyHash, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "walletID", + arg_walletPubKeyHash, + ) + } + + return result, err +} + +func (b *Bridge) WalletIDAtBlock( + arg_walletPubKeyHash [20]byte, + blockNumber *big.Int, +) ([32]byte, error) { + var result [32]byte + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "walletID", + &result, + arg_walletPubKeyHash, + ) + + return result, err +} + type walletParameters struct { WalletCreationPeriod uint32 WalletCreationMinBtcBalance uint64 @@ -6506,6 +6885,49 @@ func (b *Bridge) WalletParametersAtBlock( return result, err } +func (b *Bridge) WalletPubKeyHashForWalletID( + arg_walletId [32]byte, +) ([20]byte, error) { + result, err := b.contract.WalletPubKeyHashForWalletID( + b.callerOptions, + arg_walletId, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "walletPubKeyHashForWalletID", + arg_walletId, + ) + } + + return result, err +} + +func (b *Bridge) WalletPubKeyHashForWalletIDAtBlock( + arg_walletId [32]byte, + blockNumber *big.Int, +) ([20]byte, error) { + var result [20]byte + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "walletPubKeyHashForWalletID", + &result, + arg_walletId, + ) + + return result, err +} + func (b *Bridge) Wallets( arg_walletPubKeyHash [20]byte, ) (abi.WalletsWallet, error) { @@ -6549,6 +6971,49 @@ func (b *Bridge) WalletsAtBlock( return result, err } +func (b *Bridge) WalletsByWalletID( + arg_walletId [32]byte, +) (abi.WalletsWallet, error) { + result, err := b.contract.WalletsByWalletID( + b.callerOptions, + arg_walletId, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "walletsByWalletID", + arg_walletId, + ) + } + + return result, err +} + +func (b *Bridge) WalletsByWalletIDAtBlock( + arg_walletId [32]byte, + blockNumber *big.Int, +) (abi.WalletsWallet, error) { + var result abi.WalletsWallet + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "walletsByWalletID", + &result, + arg_walletId, + ) + + return result, err +} + // ------ Events ------- func (b *Bridge) DepositParametersUpdatedEvent( @@ -6949,9 +7414,10 @@ func (b *Bridge) PastDepositRevealedEvents( return events, nil } -func (b *Bridge) DepositsSweptEvent( +func (b *Bridge) DepositVaultFixedEvent( opts *ethereum.SubscribeOpts, -) *BDepositsSweptSubscription { + depositKeyFilter []*big.Int, +) *BDepositVaultFixedSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -6962,27 +7428,29 @@ func (b *Bridge) DepositsSweptEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BDepositsSweptSubscription{ + return &BDepositVaultFixedSubscription{ b, opts, + depositKeyFilter, } } -type BDepositsSweptSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts +type BDepositVaultFixedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + depositKeyFilter []*big.Int } -type bridgeDepositsSweptFunc func( - WalletPubKeyHash [20]byte, - SweepTxHash [32]byte, +type bridgeDepositVaultFixedFunc func( + DepositKey *big.Int, + NewVault common.Address, blockNumber uint64, ) -func (dss *BDepositsSweptSubscription) OnEvent( - handler bridgeDepositsSweptFunc, +func (dvfs *BDepositVaultFixedSubscription) OnEvent( + handler bridgeDepositVaultFixedFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeDepositsSwept) + eventChan := make(chan *abi.BridgeDepositVaultFixed) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -6992,50 +7460,51 @@ func (dss *BDepositsSweptSubscription) OnEvent( return case event := <-eventChan: handler( - event.WalletPubKeyHash, - event.SweepTxHash, + event.DepositKey, + event.NewVault, event.Raw.BlockNumber, ) } } }() - sub := dss.Pipe(eventChan) + sub := dvfs.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (dss *BDepositsSweptSubscription) Pipe( - sink chan *abi.BridgeDepositsSwept, +func (dvfs *BDepositVaultFixedSubscription) Pipe( + sink chan *abi.BridgeDepositVaultFixed, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(dss.opts.Tick) + ticker := time.NewTicker(dvfs.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := dss.contract.blockCounter.CurrentBlock() + lastBlock, err := dvfs.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - dss.opts.PastBlocks + fromBlock := lastBlock - dvfs.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past DepositsSwept events "+ + "subscription monitoring fetching past DepositVaultFixed events "+ "starting from block [%v]", fromBlock, ) - events, err := dss.contract.PastDepositsSweptEvents( + events, err := dvfs.contract.PastDepositVaultFixedEvents( fromBlock, nil, + dvfs.depositKeyFilter, ) if err != nil { bLogger.Errorf( @@ -7045,7 +7514,7 @@ func (dss *BDepositsSweptSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past DepositsSwept events", + "subscription monitoring fetched [%v] past DepositVaultFixed events", len(events), ) @@ -7056,8 +7525,9 @@ func (dss *BDepositsSweptSubscription) Pipe( } }() - sub := dss.contract.watchDepositsSwept( + sub := dvfs.contract.watchDepositVaultFixed( sink, + dvfs.depositKeyFilter, ) return subscription.NewEventSubscription(func() { @@ -7066,19 +7536,21 @@ func (dss *BDepositsSweptSubscription) Pipe( }) } -func (b *Bridge) watchDepositsSwept( - sink chan *abi.BridgeDepositsSwept, +func (b *Bridge) watchDepositVaultFixed( + sink chan *abi.BridgeDepositVaultFixed, + depositKeyFilter []*big.Int, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchDepositsSwept( + return b.contract.WatchDepositVaultFixed( &bind.WatchOpts{Context: ctx}, sink, + depositKeyFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event DepositsSwept had to be "+ + "subscription to event DepositVaultFixed had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -7087,7 +7559,7 @@ func (b *Bridge) watchDepositsSwept( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event DepositsSwept failed "+ + "subscription to event DepositVaultFixed failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -7103,24 +7575,26 @@ func (b *Bridge) watchDepositsSwept( ) } -func (b *Bridge) PastDepositsSweptEvents( +func (b *Bridge) PastDepositVaultFixedEvents( startBlock uint64, endBlock *uint64, -) ([]*abi.BridgeDepositsSwept, error) { - iterator, err := b.contract.FilterDepositsSwept( + depositKeyFilter []*big.Int, +) ([]*abi.BridgeDepositVaultFixed, error) { + iterator, err := b.contract.FilterDepositVaultFixed( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, + depositKeyFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past DepositsSwept events: [%v]", + "error retrieving past DepositVaultFixed events: [%v]", err, ) } - events := make([]*abi.BridgeDepositsSwept, 0) + events := make([]*abi.BridgeDepositVaultFixed, 0) for iterator.Next() { event := iterator.Event @@ -7130,10 +7604,9 @@ func (b *Bridge) PastDepositsSweptEvents( return events, nil } -func (b *Bridge) FraudChallengeDefeatTimedOutEvent( +func (b *Bridge) DepositsSweptEvent( opts *ethereum.SubscribeOpts, - walletPubKeyHashFilter [][20]byte, -) *BFraudChallengeDefeatTimedOutSubscription { +) *BDepositsSweptSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -7144,29 +7617,27 @@ func (b *Bridge) FraudChallengeDefeatTimedOutEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BFraudChallengeDefeatTimedOutSubscription{ + return &BDepositsSweptSubscription{ b, opts, - walletPubKeyHashFilter, } } -type BFraudChallengeDefeatTimedOutSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts - walletPubKeyHashFilter [][20]byte +type BDepositsSweptSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts } -type bridgeFraudChallengeDefeatTimedOutFunc func( +type bridgeDepositsSweptFunc func( WalletPubKeyHash [20]byte, - Sighash [32]byte, + SweepTxHash [32]byte, blockNumber uint64, ) -func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) OnEvent( - handler bridgeFraudChallengeDefeatTimedOutFunc, +func (dss *BDepositsSweptSubscription) OnEvent( + handler bridgeDepositsSweptFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeFraudChallengeDefeatTimedOut) + eventChan := make(chan *abi.BridgeDepositsSwept) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -7177,50 +7648,49 @@ func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) OnEvent( case event := <-eventChan: handler( event.WalletPubKeyHash, - event.Sighash, + event.SweepTxHash, event.Raw.BlockNumber, ) } } }() - sub := fcdtos.Pipe(eventChan) + sub := dss.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) Pipe( - sink chan *abi.BridgeFraudChallengeDefeatTimedOut, +func (dss *BDepositsSweptSubscription) Pipe( + sink chan *abi.BridgeDepositsSwept, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(fcdtos.opts.Tick) + ticker := time.NewTicker(dss.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := fcdtos.contract.blockCounter.CurrentBlock() + lastBlock, err := dss.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - fcdtos.opts.PastBlocks + fromBlock := lastBlock - dss.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past FraudChallengeDefeatTimedOut events "+ + "subscription monitoring fetching past DepositsSwept events "+ "starting from block [%v]", fromBlock, ) - events, err := fcdtos.contract.PastFraudChallengeDefeatTimedOutEvents( + events, err := dss.contract.PastDepositsSweptEvents( fromBlock, nil, - fcdtos.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -7230,7 +7700,192 @@ func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past FraudChallengeDefeatTimedOut events", + "subscription monitoring fetched [%v] past DepositsSwept events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := dss.contract.watchDepositsSwept( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchDepositsSwept( + sink chan *abi.BridgeDepositsSwept, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchDepositsSwept( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event DepositsSwept had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event DepositsSwept failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastDepositsSweptEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeDepositsSwept, error) { + iterator, err := b.contract.FilterDepositsSwept( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past DepositsSwept events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeDepositsSwept, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (b *Bridge) FraudChallengeDefeatTimedOutEvent( + opts *ethereum.SubscribeOpts, + walletPubKeyHashFilter [][20]byte, +) *BFraudChallengeDefeatTimedOutSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BFraudChallengeDefeatTimedOutSubscription{ + b, + opts, + walletPubKeyHashFilter, + } +} + +type BFraudChallengeDefeatTimedOutSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + walletPubKeyHashFilter [][20]byte +} + +type bridgeFraudChallengeDefeatTimedOutFunc func( + WalletPubKeyHash [20]byte, + Sighash [32]byte, + blockNumber uint64, +) + +func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) OnEvent( + handler bridgeFraudChallengeDefeatTimedOutFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeFraudChallengeDefeatTimedOut) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.WalletPubKeyHash, + event.Sighash, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := fcdtos.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) Pipe( + sink chan *abi.BridgeFraudChallengeDefeatTimedOut, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(fcdtos.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := fcdtos.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - fcdtos.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past FraudChallengeDefeatTimedOut events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := fcdtos.contract.PastFraudChallengeDefeatTimedOutEvents( + fromBlock, + nil, + fcdtos.walletPubKeyHashFilter, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past FraudChallengeDefeatTimedOut events", len(events), ) @@ -9977,6 +10632,216 @@ func (b *Bridge) PastNewWalletRegisteredEvents( return events, nil } +func (b *Bridge) NewWalletRegisteredV2Event( + opts *ethereum.SubscribeOpts, + walletIDFilter [][32]byte, + ecdsaWalletIDFilter [][32]byte, + walletPubKeyHashFilter [][20]byte, +) *BNewWalletRegisteredV2Subscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BNewWalletRegisteredV2Subscription{ + b, + opts, + walletIDFilter, + ecdsaWalletIDFilter, + walletPubKeyHashFilter, + } +} + +type BNewWalletRegisteredV2Subscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + walletIDFilter [][32]byte + ecdsaWalletIDFilter [][32]byte + walletPubKeyHashFilter [][20]byte +} + +type bridgeNewWalletRegisteredV2Func func( + WalletID [32]byte, + EcdsaWalletID [32]byte, + WalletPubKeyHash [20]byte, + blockNumber uint64, +) + +func (nwrvs *BNewWalletRegisteredV2Subscription) OnEvent( + handler bridgeNewWalletRegisteredV2Func, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeNewWalletRegisteredV2) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.WalletID, + event.EcdsaWalletID, + event.WalletPubKeyHash, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := nwrvs.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (nwrvs *BNewWalletRegisteredV2Subscription) Pipe( + sink chan *abi.BridgeNewWalletRegisteredV2, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(nwrvs.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := nwrvs.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - nwrvs.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past NewWalletRegisteredV2 events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := nwrvs.contract.PastNewWalletRegisteredV2Events( + fromBlock, + nil, + nwrvs.walletIDFilter, + nwrvs.ecdsaWalletIDFilter, + nwrvs.walletPubKeyHashFilter, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past NewWalletRegisteredV2 events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := nwrvs.contract.watchNewWalletRegisteredV2( + sink, + nwrvs.walletIDFilter, + nwrvs.ecdsaWalletIDFilter, + nwrvs.walletPubKeyHashFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchNewWalletRegisteredV2( + sink chan *abi.BridgeNewWalletRegisteredV2, + walletIDFilter [][32]byte, + ecdsaWalletIDFilter [][32]byte, + walletPubKeyHashFilter [][20]byte, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchNewWalletRegisteredV2( + &bind.WatchOpts{Context: ctx}, + sink, + walletIDFilter, + ecdsaWalletIDFilter, + walletPubKeyHashFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event NewWalletRegisteredV2 had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event NewWalletRegisteredV2 failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastNewWalletRegisteredV2Events( + startBlock uint64, + endBlock *uint64, + walletIDFilter [][32]byte, + ecdsaWalletIDFilter [][32]byte, + walletPubKeyHashFilter [][20]byte, +) ([]*abi.BridgeNewWalletRegisteredV2, error) { + iterator, err := b.contract.FilterNewWalletRegisteredV2( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + walletIDFilter, + ecdsaWalletIDFilter, + walletPubKeyHashFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past NewWalletRegisteredV2 events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeNewWalletRegisteredV2, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + func (b *Bridge) NewWalletRequestedEvent( opts *ethereum.SubscribeOpts, ) *BNewWalletRequestedSubscription { @@ -10154,6 +11019,185 @@ func (b *Bridge) PastNewWalletRequestedEvents( return events, nil } +func (b *Bridge) RebateStakingSetEvent( + opts *ethereum.SubscribeOpts, +) *BRebateStakingSetSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BRebateStakingSetSubscription{ + b, + opts, + } +} + +type BRebateStakingSetSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts +} + +type bridgeRebateStakingSetFunc func( + RebateStaking common.Address, + blockNumber uint64, +) + +func (rsss *BRebateStakingSetSubscription) OnEvent( + handler bridgeRebateStakingSetFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeRebateStakingSet) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.RebateStaking, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := rsss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (rsss *BRebateStakingSetSubscription) Pipe( + sink chan *abi.BridgeRebateStakingSet, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(rsss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := rsss.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - rsss.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past RebateStakingSet events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := rsss.contract.PastRebateStakingSetEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past RebateStakingSet events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := rsss.contract.watchRebateStakingSet( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchRebateStakingSet( + sink chan *abi.BridgeRebateStakingSet, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchRebateStakingSet( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event RebateStakingSet had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event RebateStakingSet failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastRebateStakingSetEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeRebateStakingSet, error) { + iterator, err := b.contract.FilterRebateStakingSet( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past RebateStakingSet events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeRebateStakingSet, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + func (b *Bridge) RedemptionParametersUpdatedEvent( opts *ethereum.SubscribeOpts, ) *BRedemptionParametersUpdatedSubscription { diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index f6d2a83238..76a016c019 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -257,6 +257,10 @@ type BridgeChain interface { // if the wallet was not found. GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error) + // WalletPublicKeyHashForWalletID resolves canonical wallet ID to the + // 20-byte compatibility wallet public key hash used by legacy interfaces. + WalletPublicKeyHashForWalletID(walletID [32]byte) ([20]byte, error) + // OnWalletClosed registers a callback that is invoked when an on-chain // notification of the wallet closed is seen. The notification occurs when // the wallet is closed or terminated. @@ -342,6 +346,7 @@ type NewWalletRegisteredEvent struct { type NewWalletRegisteredEventFilter struct { StartBlock uint64 EndBlock *uint64 + WalletID [][32]byte EcdsaWalletID [][32]byte WalletPublicKeyHash [][20]byte } diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index d4850bf29a..e4864c4575 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -892,6 +892,25 @@ func (lc *localChain) GetWallet(walletPublicKeyHash [20]byte) ( return walletChainData, nil } +func (lc *localChain) WalletPublicKeyHashForWalletID( + walletID [32]byte, +) ([20]byte, error) { + lc.walletsMutex.Lock() + defer lc.walletsMutex.Unlock() + + for walletPublicKeyHash, walletData := range lc.wallets { + if walletData == nil { + continue + } + + if walletID == walletData.WalletID || walletID == walletData.EcdsaWalletID { + return walletPublicKeyHash, nil + } + } + + return [20]byte{}, fmt.Errorf("wallet not found") +} + func (lc *localChain) IsWalletRegistered(EcdsaWalletID [32]byte) (bool, error) { lc.walletsMutex.Lock() defer lc.walletsMutex.Unlock() diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 6d9abda544..3af92d05d2 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -1196,22 +1196,50 @@ func (n *node) archiveClosedWallets() error { for _, walletPublicKey := range walletPublicKeys { walletPublicKeyHash := bitcoin.PublicKeyHash(walletPublicKey) - walletID, err := n.chain.CalculateWalletID(walletPublicKey) + var walletID [32]byte + var ecdsaWalletID [32]byte + + walletChainData, err := n.chain.GetWallet(walletPublicKeyHash) if err != nil { - return fmt.Errorf( - "could not calculate wallet ID for wallet with public key "+ - "hash [0x%x]: [%v]", - walletPublicKeyHash, - err, - ) + walletID, err = n.chain.CalculateWalletID(walletPublicKey) + if err != nil { + return fmt.Errorf( + "could not resolve wallet IDs for wallet with public key "+ + "hash [0x%x]: [%v]", + walletPublicKeyHash, + err, + ) + } + + // Legacy fallback for deployments where canonical wallet lookup + // is unavailable. + ecdsaWalletID = walletID + } else { + walletID = walletChainData.WalletID + if walletID == [32]byte{} { + walletID = DeriveLegacyWalletID(walletPublicKeyHash) + } + + ecdsaWalletID = walletChainData.EcdsaWalletID + if ecdsaWalletID == [32]byte{} { + ecdsaWalletID, err = n.chain.CalculateWalletID(walletPublicKey) + if err != nil { + return fmt.Errorf( + "could not calculate ECDSA wallet ID for wallet with public key "+ + "hash [0x%x]: [%v]", + walletPublicKeyHash, + err, + ) + } + } } - isRegistered, err := n.chain.IsWalletRegistered(walletID) + isRegistered, err := n.chain.IsWalletRegistered(ecdsaWalletID) if err != nil { return fmt.Errorf( - "could not check if wallet is registered for wallet with ID "+ + "could not check if wallet is registered for wallet with ECDSA ID "+ "[0x%x]: [%v]", - walletPublicKeyHash, + ecdsaWalletID, err, ) } @@ -1283,20 +1311,43 @@ func (n *node) handleWalletClosure(walletID [32]byte) error { return fmt.Errorf("wallet closure not confirmed") } - wallet, ok := n.walletRegistry.getWalletByID(walletID) + walletPublicKeyHash, err := n.chain.WalletPublicKeyHashForWalletID(walletID) + if err != nil { + logger.Warnf( + "cannot resolve wallet public key hash for wallet ID [0x%x]: [%v]; "+ + "falling back to local wallet ID matching", + walletID, + err, + ) + + wallet, ok := n.walletRegistry.getWalletByID(walletID) + if !ok { + // Wallet was not found in the registry. The wallet is not controlled + // by this node. + logger.Infof( + "node does not control wallet with ID [0x%x]; quitting wallet "+ + "archiving", + walletID, + ) + return nil + } + + walletPublicKeyHash = bitcoin.PublicKeyHash(wallet.publicKey) + } + + _, ok := n.walletRegistry.getWalletByPublicKeyHash(walletPublicKeyHash) if !ok { // Wallet was not found in the registry. The wallet is not controlled by // this node. logger.Infof( - "node does not control wallet with ID [0x%x]; quitting wallet "+ - "archiving", + "node does not control wallet with ID [0x%x] and public key hash "+ + "[0x%x]; quitting wallet archiving", walletID, + walletPublicKeyHash, ) return nil } - walletPublicKeyHash := bitcoin.PublicKeyHash(wallet.publicKey) - err = n.walletRegistry.archiveWallet(walletPublicKeyHash) if err != nil { return fmt.Errorf("failed to archive the wallet: [%v]", err) diff --git a/pkg/tbtc/wallet_id.go b/pkg/tbtc/wallet_id.go index e82177dea4..6605b762fe 100644 --- a/pkg/tbtc/wallet_id.go +++ b/pkg/tbtc/wallet_id.go @@ -10,3 +10,21 @@ func DeriveLegacyWalletID(walletPublicKeyHash [20]byte) [32]byte { copy(walletID[12:], walletPublicKeyHash[:]) return walletID } + +// WalletPublicKeyHashFromLegacyWalletID extracts the compatibility wallet +// public key hash from a canonical legacy wallet ID. +// +// Legacy wallet ID format is a left-padded bytes20 hash: +// bytes32(uint256(uint160(walletPubKeyHash))). +func WalletPublicKeyHashFromLegacyWalletID(walletID [32]byte) ([20]byte, bool) { + for i := 0; i < 12; i++ { + if walletID[i] != 0 { + return [20]byte{}, false + } + } + + var walletPublicKeyHash [20]byte + copy(walletPublicKeyHash[:], walletID[12:]) + + return walletPublicKeyHash, true +} diff --git a/pkg/tbtc/wallet_id_test.go b/pkg/tbtc/wallet_id_test.go index 63577f8449..eb6ee3688e 100644 --- a/pkg/tbtc/wallet_id_test.go +++ b/pkg/tbtc/wallet_id_test.go @@ -35,3 +35,55 @@ func TestDeriveLegacyWalletID(t *testing.T) { ) } } + +func TestWalletPublicKeyHashFromLegacyWalletID(t *testing.T) { + walletIDBytes, err := hex.DecodeString( + "000000000000000000000000e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + ) + if err != nil { + t.Fatalf("failed to decode wallet ID: [%v]", err) + } + + var walletID [32]byte + copy(walletID[:], walletIDBytes) + + expectedWalletPublicKeyHashBytes, err := hex.DecodeString( + "e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + ) + if err != nil { + t.Fatalf("failed to decode expected wallet public key hash: [%v]", err) + } + + var expectedWalletPublicKeyHash [20]byte + copy(expectedWalletPublicKeyHash[:], expectedWalletPublicKeyHashBytes) + + actualWalletPublicKeyHash, ok := WalletPublicKeyHashFromLegacyWalletID(walletID) + if !ok { + t.Fatal("expected wallet ID to be recognized as legacy") + } + + if actualWalletPublicKeyHash != expectedWalletPublicKeyHash { + t.Fatalf( + "unexpected wallet public key hash\nexpected: [%x]\nactual: [%x]", + expectedWalletPublicKeyHash, + actualWalletPublicKeyHash, + ) + } +} + +func TestWalletPublicKeyHashFromLegacyWalletID_NonLegacy(t *testing.T) { + walletIDBytes, err := hex.DecodeString( + "010000000000000000000000e6f9d74726b19b75f16fe1e9feaec048aa4fa1d0", + ) + if err != nil { + t.Fatalf("failed to decode wallet ID: [%v]", err) + } + + var walletID [32]byte + copy(walletID[:], walletIDBytes) + + _, ok := WalletPublicKeyHashFromLegacyWalletID(walletID) + if ok { + t.Fatal("expected wallet ID to be recognized as non-legacy") + } +} From bbd1b53cda951dfe37da0d568ddd125b220c1f06 Mon Sep 17 00:00:00 2001 From: maclane Date: Sat, 21 Feb 2026 17:49:56 -0600 Subject: [PATCH 39/96] tbtc: keep bridge address embed placeholder empty --- pkg/chain/ethereum/tbtc/gen/_address/Bridge | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/chain/ethereum/tbtc/gen/_address/Bridge b/pkg/chain/ethereum/tbtc/gen/_address/Bridge index 7daa69d34b..e69de29bb2 100644 --- a/pkg/chain/ethereum/tbtc/gen/_address/Bridge +++ b/pkg/chain/ethereum/tbtc/gen/_address/Bridge @@ -1 +0,0 @@ -0x0000000000000000000000000000000000000000 From b4185499cad0c42272d7cd556a95b14e8d711a41 Mon Sep 17 00:00:00 2001 From: maclane Date: Sat, 21 Feb 2026 19:33:50 -0600 Subject: [PATCH 40/96] tbtc: lower expected wallet closure resolution miss to debug --- pkg/tbtc/node.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 3af92d05d2..b13f831d73 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -1313,7 +1313,11 @@ func (n *node) handleWalletClosure(walletID [32]byte) error { walletPublicKeyHash, err := n.chain.WalletPublicKeyHashForWalletID(walletID) if err != nil { - logger.Warnf( + // WalletClosed events still carry ECDSA wallet IDs from the legacy + // registry path. Until closure events are emitted with canonical IDs, + // canonical wallet-ID resolution is expected to miss and we use the + // local registry fallback below. + logger.Debugf( "cannot resolve wallet public key hash for wallet ID [0x%x]: [%v]; "+ "falling back to local wallet ID matching", walletID, From 5d0b9da9f2ecf71637013a28693fdfd64409e941 Mon Sep 17 00:00:00 2001 From: maclane Date: Sat, 21 Feb 2026 19:39:15 -0600 Subject: [PATCH 41/96] tbtc: add wallet-id fallback coverage for ethereum adapter --- pkg/chain/ethereum/tbtc.go | 57 ++++++- pkg/chain/ethereum/tbtc_test.go | 286 ++++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+), 4 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 9ae4192b15..f9d0d30c17 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1386,7 +1386,42 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( walletPublicKeyHash = filter.WalletPublicKeyHash } - v2Events, err := tc.bridge.PastNewWalletRegisteredV2Events( + return pastNewWalletRegisteredEvents( + startBlock, + endBlock, + walletID, + ecdsaWalletID, + walletPublicKeyHash, + tc.bridge.PastNewWalletRegisteredV2Events, + tc.bridge.PastNewWalletRegisteredEvents, + ) +} + +type pastNewWalletRegisteredV2EventsFn func( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPubKeyHash [][20]byte, +) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) + +type pastNewWalletRegisteredEventsFn func( + startBlock uint64, + endBlock *uint64, + ecdsaWalletID [][32]byte, + walletPubKeyHash [][20]byte, +) ([]*tbtcabi.BridgeNewWalletRegistered, error) + +func pastNewWalletRegisteredEvents( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPublicKeyHash [][20]byte, + pastV2Events pastNewWalletRegisteredV2EventsFn, + pastLegacyEvents pastNewWalletRegisteredEventsFn, +) ([]*tbtc.NewWalletRegisteredEvent, error) { + v2Events, err := pastV2Events( startBlock, endBlock, walletID, @@ -1411,7 +1446,7 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( // Fallback for legacy deployments that do not emit NewWalletRegisteredV2. if len(convertedEvents) == 0 && len(walletID) == 0 { - legacyEvents, err := tc.bridge.PastNewWalletRegisteredEvents( + legacyEvents, err := pastLegacyEvents( startBlock, endBlock, ecdsaWalletID, @@ -1440,7 +1475,7 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( }, ) - return convertedEvents, err + return convertedEvents, nil } func (tc *TbtcChain) CalculateWalletID( @@ -1524,7 +1559,21 @@ func (tc *TbtcChain) GetWallet( func (tc *TbtcChain) WalletPublicKeyHashForWalletID( walletID [32]byte, ) ([20]byte, error) { - walletPublicKeyHash, err := tc.bridge.WalletPubKeyHashForWalletID(walletID) + return resolveWalletPublicKeyHashForWalletID( + walletID, + tc.bridge.WalletPubKeyHashForWalletID, + ) +} + +type walletPublicKeyHashForWalletIDFn func( + walletID [32]byte, +) ([20]byte, error) + +func resolveWalletPublicKeyHashForWalletID( + walletID [32]byte, + resolveCanonical walletPublicKeyHashForWalletIDFn, +) ([20]byte, error) { + walletPublicKeyHash, err := resolveCanonical(walletID) if err == nil { if walletPublicKeyHash != [20]byte{} { return walletPublicKeyHash, nil diff --git a/pkg/chain/ethereum/tbtc_test.go b/pkg/chain/ethereum/tbtc_test.go index 1c9eef1be0..cf94830ea3 100644 --- a/pkg/chain/ethereum/tbtc_test.go +++ b/pkg/chain/ethereum/tbtc_test.go @@ -4,12 +4,17 @@ import ( "bytes" "crypto/ecdsa" "encoding/hex" + "errors" "fmt" "math/big" "reflect" + "strings" "testing" + "github.com/ethereum/go-ethereum/core/types" "github.com/keep-network/keep-core/pkg/bitcoin" + tbtcabi "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi" + tbtcpkg "github.com/keep-network/keep-core/pkg/tbtc" "github.com/keep-network/keep-core/pkg/chain" @@ -323,6 +328,287 @@ func TestCalculateWalletID(t *testing.T) { testutils.AssertBytesEqual(t, expectedWalletID[:], actualWalletID[:]) } +func TestPastNewWalletRegisteredEvents_UsesV2EventsWhenAvailable(t *testing.T) { + startBlock := uint64(500) + endBlock := uint64(700) + + expectedWalletIDA := [32]byte{0xaa} + expectedWalletIDB := [32]byte{0xbb} + + expectedECDSAWalletIDA := [32]byte{0xa1} + expectedECDSAWalletIDB := [32]byte{0xb1} + + expectedWalletPublicKeyHashA := [20]byte{0x11} + expectedWalletPublicKeyHashB := [20]byte{0x22} + + legacyFallbackCalled := false + + actualEvents, err := pastNewWalletRegisteredEvents( + startBlock, + &endBlock, + nil, + nil, + nil, + func( + actualStartBlock uint64, + actualEndBlock *uint64, + _ [][32]byte, + _ [][32]byte, + _ [][20]byte, + ) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { + if actualStartBlock != startBlock { + t.Fatalf("unexpected start block: [%v]", actualStartBlock) + } + + if actualEndBlock == nil || *actualEndBlock != endBlock { + t.Fatalf("unexpected end block: [%v]", actualEndBlock) + } + + // Provide events out of order to verify post-conversion sort. + return []*tbtcabi.BridgeNewWalletRegisteredV2{ + { + WalletID: expectedWalletIDB, + EcdsaWalletID: expectedECDSAWalletIDB, + WalletPubKeyHash: expectedWalletPublicKeyHashB, + Raw: types.Log{BlockNumber: 650}, + }, + { + WalletID: expectedWalletIDA, + EcdsaWalletID: expectedECDSAWalletIDA, + WalletPubKeyHash: expectedWalletPublicKeyHashA, + Raw: types.Log{BlockNumber: 600}, + }, + }, nil + }, + func(uint64, *uint64, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegistered, error) { + legacyFallbackCalled = true + return nil, nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if legacyFallbackCalled { + t.Fatal("legacy fallback should not be called when v2 events are present") + } + + if len(actualEvents) != 2 { + t.Fatalf("unexpected events count: [%v]", len(actualEvents)) + } + + // Expect ascending block order after conversion. + if actualEvents[0].BlockNumber != 600 || actualEvents[1].BlockNumber != 650 { + t.Fatalf( + "unexpected event ordering by block: [%v], [%v]", + actualEvents[0].BlockNumber, + actualEvents[1].BlockNumber, + ) + } + + if actualEvents[0].WalletID != expectedWalletIDA || + actualEvents[1].WalletID != expectedWalletIDB { + t.Fatal("unexpected wallet IDs in converted events") + } +} + +func TestPastNewWalletRegisteredEvents_FallsBackToLegacyWhenV2Empty(t *testing.T) { + expectedECDSAWalletID := [32]byte{0xdd} + expectedWalletPublicKeyHash := [20]byte{0xee} + + legacyFallbackCalled := false + + actualEvents, err := pastNewWalletRegisteredEvents( + 1, + nil, + nil, // no canonical wallet-ID filter -> fallback path enabled + nil, + nil, + func(uint64, *uint64, [][32]byte, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { + return []*tbtcabi.BridgeNewWalletRegisteredV2{}, nil + }, + func(uint64, *uint64, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegistered, error) { + legacyFallbackCalled = true + return []*tbtcabi.BridgeNewWalletRegistered{ + { + EcdsaWalletID: expectedECDSAWalletID, + WalletPubKeyHash: expectedWalletPublicKeyHash, + Raw: types.Log{BlockNumber: 1000}, + }, + }, nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if !legacyFallbackCalled { + t.Fatal("legacy fallback should be called when v2 events are empty") + } + + if len(actualEvents) != 1 { + t.Fatalf("unexpected events count: [%v]", len(actualEvents)) + } + + expectedWalletID := tbtcpkg.DeriveLegacyWalletID(expectedWalletPublicKeyHash) + if actualEvents[0].WalletID != expectedWalletID { + t.Fatalf( + "unexpected derived legacy wallet ID\nexpected: [%x]\nactual: [%x]", + expectedWalletID, + actualEvents[0].WalletID, + ) + } +} + +func TestPastNewWalletRegisteredEvents_DoesNotFallbackWithWalletIDFilter(t *testing.T) { + legacyFallbackCalled := false + + walletIDFilter := [][32]byte{ + {0x1}, + } + + actualEvents, err := pastNewWalletRegisteredEvents( + 1, + nil, + walletIDFilter, + nil, + nil, + func(uint64, *uint64, [][32]byte, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { + return []*tbtcabi.BridgeNewWalletRegisteredV2{}, nil + }, + func(uint64, *uint64, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegistered, error) { + legacyFallbackCalled = true + return nil, nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if legacyFallbackCalled { + t.Fatal("legacy fallback should be skipped when walletID filter is provided") + } + + if len(actualEvents) != 0 { + t.Fatalf("unexpected events count: [%v]", len(actualEvents)) + } +} + +func TestResolveWalletPublicKeyHashForWalletID(t *testing.T) { + t.Run("returns canonical mapping when non-zero", func(t *testing.T) { + walletID := [32]byte{0x01} + expectedWalletPublicKeyHash := [20]byte{0xaa} + + actualWalletPublicKeyHash, err := resolveWalletPublicKeyHashForWalletID( + walletID, + func(actualWalletID [32]byte) ([20]byte, error) { + if actualWalletID != walletID { + t.Fatalf("unexpected wallet ID: [%x]", actualWalletID) + } + + return expectedWalletPublicKeyHash, nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if actualWalletPublicKeyHash != expectedWalletPublicKeyHash { + t.Fatalf( + "unexpected wallet public key hash\nexpected: [%x]\nactual: [%x]", + expectedWalletPublicKeyHash, + actualWalletPublicKeyHash, + ) + } + }) + + t.Run("falls back to legacy extraction when canonical lookup errors", func(t *testing.T) { + expectedWalletPublicKeyHash := [20]byte{0xbb} + legacyWalletID := tbtcpkg.DeriveLegacyWalletID(expectedWalletPublicKeyHash) + + actualWalletPublicKeyHash, err := resolveWalletPublicKeyHashForWalletID( + legacyWalletID, + func([32]byte) ([20]byte, error) { + return [20]byte{}, errors.New("canonical lookup unavailable") + }, + ) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if actualWalletPublicKeyHash != expectedWalletPublicKeyHash { + t.Fatalf( + "unexpected wallet public key hash\nexpected: [%x]\nactual: [%x]", + expectedWalletPublicKeyHash, + actualWalletPublicKeyHash, + ) + } + }) + + t.Run("falls back to legacy extraction when canonical lookup returns zero", func(t *testing.T) { + expectedWalletPublicKeyHash := [20]byte{0xbc} + legacyWalletID := tbtcpkg.DeriveLegacyWalletID(expectedWalletPublicKeyHash) + + actualWalletPublicKeyHash, err := resolveWalletPublicKeyHashForWalletID( + legacyWalletID, + func([32]byte) ([20]byte, error) { + return [20]byte{}, nil + }, + ) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if actualWalletPublicKeyHash != expectedWalletPublicKeyHash { + t.Fatalf( + "unexpected wallet public key hash\nexpected: [%x]\nactual: [%x]", + expectedWalletPublicKeyHash, + actualWalletPublicKeyHash, + ) + } + }) + + t.Run("returns wrapped canonical error for non-legacy IDs", func(t *testing.T) { + walletID := [32]byte{0xff} + canonicalErr := errors.New("rpc failure") + + _, err := resolveWalletPublicKeyHashForWalletID( + walletID, + func([32]byte) ([20]byte, error) { + return [20]byte{}, canonicalErr + }, + ) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "cannot resolve wallet public key hash") { + t.Fatalf("unexpected error: [%v]", err) + } + if !strings.Contains(err.Error(), canonicalErr.Error()) { + t.Fatalf("expected canonical error to be wrapped: [%v]", err) + } + }) + + t.Run("returns not found for non-legacy IDs when canonical lookup returns zero", func(t *testing.T) { + walletID := [32]byte{0xfe} + + _, err := resolveWalletPublicKeyHashForWalletID( + walletID, + func([32]byte) ([20]byte, error) { + return [20]byte{}, nil + }, + ) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "wallet public key hash not found") { + t.Fatalf("unexpected error: [%v]", err) + } + }) +} + func TestParseDkgResultValidationOutcome(t *testing.T) { isValid, err := parseDkgResultValidationOutcome( &struct { From 8f9016d21b97eebaf9fb2b0a18ee4810d7ae239e Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 11:16:56 -0600 Subject: [PATCH 42/96] feat(frost): scaffold tbtc-signer native engine registration --- ...e_tbtc_signer_registration_frost_native.go | 63 +++++++++++++++++++ ...c_signer_registration_frost_native_test.go | 37 +++++++++++ ...niffi_registration_frost_native_default.go | 2 +- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go create mode 100644 pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go new file mode 100644 index 0000000000..55921ac3f7 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -0,0 +1,63 @@ +//go:build frost_native && frost_tbtc_signer && cgo && !frost_uniffi_sdk + +package signing + +import "fmt" + +type buildTaggedTBTCSignerNativeFROSTBridge struct{} + +func registerBuildTaggedNativeFROSTSigningEngine() error { + engine, err := newUniFFINativeFROSTSigningEngine( + &buildTaggedTBTCSignerNativeFROSTBridge{}, + ) + if err != nil { + return err + } + + return RegisterNativeFROSTSigningEngine(engine) +} + +func (bttsnfb *buildTaggedTBTCSignerNativeFROSTBridge) GenerateNoncesAndCommitments( + keyPackageIdentifier string, + keyPackageData []byte, +) ( + noncesData []byte, + commitmentIdentifier string, + commitmentData []byte, + err error, +) { + return nil, "", nil, buildTaggedTBTCSignerBridgeNotImplementedError( + "GenerateNoncesAndCommitments", + ) +} + +func (bttsnfb *buildTaggedTBTCSignerNativeFROSTBridge) NewSigningPackage( + message []byte, + commitments []uniFFINativeFROSTCommitment, +) (signingPackageData []byte, err error) { + return nil, buildTaggedTBTCSignerBridgeNotImplementedError("NewSigningPackage") +} + +func (bttsnfb *buildTaggedTBTCSignerNativeFROSTBridge) Sign( + signingPackageData []byte, + noncesData []byte, + keyPackageIdentifier string, + keyPackageData []byte, +) (signatureShareIdentifier string, signatureShareData []byte, err error) { + return "", nil, buildTaggedTBTCSignerBridgeNotImplementedError("Sign") +} + +func (bttsnfb *buildTaggedTBTCSignerNativeFROSTBridge) Aggregate( + signingPackageData []byte, + signatureShares []uniFFINativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, +) (signature []byte, err error) { + return nil, buildTaggedTBTCSignerBridgeNotImplementedError("Aggregate") +} + +func buildTaggedTBTCSignerBridgeNotImplementedError(operation string) error { + return fmt.Errorf( + "tbtc-signer bridge operation [%v] is not implemented", + operation, + ) +} diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go new file mode 100644 index 0000000000..d0121835e6 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -0,0 +1,37 @@ +//go:build frost_native && frost_tbtc_signer && cgo && !frost_uniffi_sdk + +package signing + +import ( + "strings" + "testing" +) + +func TestRegisterBuildTaggedTBTCSignerNativeFROSTSigningEngine(t *testing.T) { + UnregisterNativeFROSTSigningEngine() + t.Cleanup(func() { + UnregisterNativeFROSTSigningEngine() + }) + + err := registerBuildTaggedNativeFROSTSigningEngine() + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + engine := currentNativeFROSTSigningEngine() + if engine == nil { + t.Fatal("expected native FROST signing engine registration") + } + + _, _, err = engine.GenerateNoncesAndCommitments(&NativeFROSTKeyPackage{ + Identifier: "participant-1", + Data: []byte{1, 2, 3}, + }) + if err == nil { + t.Fatal("expected not-implemented tbtc-signer bridge error") + } + + if !strings.Contains(err.Error(), "not implemented") { + t.Fatalf("unexpected bridge error: [%v]", err) + } +} diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go index 673483a929..532c86b3fa 100644 --- a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go @@ -1,4 +1,4 @@ -//go:build frost_native && !(frost_uniffi_sdk && cgo) +//go:build frost_native && !(frost_uniffi_sdk && cgo) && !(frost_tbtc_signer && cgo) package signing From db36fa061d69ead9943d0f19b47c3b2db4197632 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 12:07:27 -0600 Subject: [PATCH 43/96] refactor(frost): scaffold coarse tbtc-signer session engine path --- ...ffi_primitive_transitional_frost_native.go | 94 +++++++++- ...rimitive_transitional_frost_native_test.go | 164 ++++++++++++++++++ ...e_tbtc_signer_registration_frost_native.go | 54 ++---- ...c_signer_registration_frost_native_test.go | 19 +- .../native_tbtc_signer_engine_frost_native.go | 74 ++++++++ ...ve_tbtc_signer_engine_frost_native_test.go | 66 +++++++ 6 files changed, 419 insertions(+), 52 deletions(-) create mode 100644 pkg/frost/signing/native_tbtc_signer_engine_frost_native.go create mode 100644 pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index dfc6f3cbeb..cd2944fee1 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -4,6 +4,7 @@ package signing import ( "context" + "encoding/json" "fmt" "github.com/ipfs/go-log/v2" @@ -28,7 +29,8 @@ func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( // buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive is a // transitional primitive that executes native two-round FROST when // `frost-uniffi-v2` signer material is provided, and preserves legacy bridge -// execution for `frost-uniffi-v1` payloads. +// execution for `frost-uniffi-v1` payloads. `frost-tbtc-signer-v1` is reserved +// for coarse session engine integration and currently returns a scaffold error. type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) Sign( @@ -71,6 +73,9 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) case NativeSignerMaterialFormatFrostUniFFIV1: return btlcnnefsp.signWithLegacyTECDSABridge(ctx, logger, request) + case NativeSignerMaterialFormatFrostTBTCSignerV1: + return btlcnnefsp.signWithTBTCSignerCoarseEngine(ctx, logger, request) + default: return nil, fmt.Errorf( "%w: unsupported signer material format: [%s]", @@ -80,6 +85,45 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) } } +func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithTBTCSignerCoarseEngine( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + keyGroup, err := decodeBuildTaggedTBTCSignerKeyGroup(request.SignerMaterial) + if err != nil { + return nil, err + } + + engine := currentNativeTBTCSignerEngine() + if engine == nil { + return nil, fmt.Errorf( + "%w: native tbtc-signer engine is unavailable", + ErrNativeCryptographyUnavailable, + ) + } + + _, err = engine.StartSignRound( + request.SessionID, + request.Message.Bytes(), + keyGroup, + ) + if err != nil { + return nil, fmt.Errorf( + "%w: tbtc-signer StartSignRound failed: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + // The coarse-session finalize flow is intentionally deferred until keep-core + // transport/orchestration is migrated from round-level message exchange. + return nil, fmt.Errorf( + "%w: tbtc-signer coarse session finalize flow is not wired", + ErrNativeCryptographyUnavailable, + ) +} + func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithLegacyTECDSABridge( ctx context.Context, logger log.StandardLogger, @@ -160,3 +204,51 @@ func decodeBuildTaggedLegacyPrivateKeyShare( return privateKeyShare, nil } + +type buildTaggedTBTCSignerMaterialPayload struct { + KeyGroup string `json:"keyGroup"` +} + +func decodeBuildTaggedTBTCSignerKeyGroup( + signerMaterial *NativeSignerMaterial, +) (string, error) { + if signerMaterial == nil { + return "", fmt.Errorf( + "%w: signer material is nil", + ErrNativeCryptographyUnavailable, + ) + } + + if signerMaterial.Format != NativeSignerMaterialFormatFrostTBTCSignerV1 { + return "", fmt.Errorf( + "%w: unsupported signer material format: [%s]", + ErrNativeCryptographyUnavailable, + signerMaterial.Format, + ) + } + + if len(signerMaterial.Payload) == 0 { + return "", fmt.Errorf( + "%w: signer material payload is empty", + ErrNativeCryptographyUnavailable, + ) + } + + var payload buildTaggedTBTCSignerMaterialPayload + if err := json.Unmarshal(signerMaterial.Payload, &payload); err != nil { + return "", fmt.Errorf( + "%w: cannot unmarshal tbtc-signer payload: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if payload.KeyGroup == "" { + return "", fmt.Errorf( + "%w: tbtc-signer key group is empty", + ErrNativeCryptographyUnavailable, + ) + } + + return payload.KeyGroup, nil +} diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 8e88ddce4a..454a2601e8 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -5,6 +5,7 @@ package signing import ( "bytes" "errors" + "fmt" "math/big" "testing" @@ -12,6 +13,38 @@ import ( "github.com/keep-network/keep-core/pkg/tecdsa" ) +type mockBuildTaggedTBTCSignerEngine struct { + startCalled bool + sessionID string + message []byte + keyGroup string +} + +func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( + sessionID string, + message []byte, + keyGroup string, +) (*NativeTBTCSignerRoundState, error) { + mbttse.startCalled = true + mbttse.sessionID = sessionID + mbttse.message = append([]byte{}, message...) + mbttse.keyGroup = keyGroup + + return &NativeTBTCSignerRoundState{ + SessionID: sessionID, + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "00", + }, nil +} + +func (mbttse *mockBuildTaggedTBTCSignerEngine) FinalizeSignRound( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, +) ([]byte, error) { + return nil, fmt.Errorf("not used") +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_ValidatesRequest( t *testing.T, ) { @@ -141,3 +174,134 @@ func TestDecodeBuildTaggedLegacyPrivateKeyShare_RejectsInvalidMaterial( }) } } + +func TestDecodeBuildTaggedTBTCSignerKeyGroup(t *testing.T) { + keyGroup, err := decodeBuildTaggedTBTCSignerKeyGroup(&NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":"group-1"}`), + }) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + if keyGroup != "group-1" { + t.Fatalf( + "unexpected key group\nexpected: [%v]\nactual: [%v]", + "group-1", + keyGroup, + ) + } +} + +func TestDecodeBuildTaggedTBTCSignerKeyGroup_RejectsInvalidMaterial( + t *testing.T, +) { + testCases := []struct { + name string + signerMaterial *NativeSignerMaterial + }{ + { + name: "nil signer material", + signerMaterial: nil, + }, + { + name: "unsupported format", + signerMaterial: &NativeSignerMaterial{ + Format: "other", + Payload: []byte(`{"keyGroup":"group-1"}`), + }, + }, + { + name: "empty payload", + signerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + }, + }, + { + name: "invalid payload", + signerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":`), + }, + }, + { + name: "empty key group", + signerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":""}`), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := decodeBuildTaggedTBTCSignerKeyGroup(tc.signerMaterial) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + }) + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath( + t *testing.T, +) { + engine := &mockBuildTaggedTBTCSignerEngine{} + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":"group-1"}`), + }, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if !engine.startCalled { + t.Fatal("expected StartSignRound call") + } + + if engine.sessionID != "session-1" { + t.Fatalf( + "unexpected session ID\nexpected: [%v]\nactual: [%v]", + "session-1", + engine.sessionID, + ) + } + + if engine.keyGroup != "group-1" { + t.Fatalf( + "unexpected key group\nexpected: [%v]\nactual: [%v]", + "group-1", + engine.keyGroup, + ) + } +} diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index 55921ac3f7..f31164e488 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -4,55 +4,25 @@ package signing import "fmt" -type buildTaggedTBTCSignerNativeFROSTBridge struct{} +type buildTaggedTBTCSignerEngine struct{} func registerBuildTaggedNativeFROSTSigningEngine() error { - engine, err := newUniFFINativeFROSTSigningEngine( - &buildTaggedTBTCSignerNativeFROSTBridge{}, - ) - if err != nil { - return err - } - - return RegisterNativeFROSTSigningEngine(engine) + return RegisterNativeTBTCSignerEngine(&buildTaggedTBTCSignerEngine{}) } -func (bttsnfb *buildTaggedTBTCSignerNativeFROSTBridge) GenerateNoncesAndCommitments( - keyPackageIdentifier string, - keyPackageData []byte, -) ( - noncesData []byte, - commitmentIdentifier string, - commitmentData []byte, - err error, -) { - return nil, "", nil, buildTaggedTBTCSignerBridgeNotImplementedError( - "GenerateNoncesAndCommitments", - ) -} - -func (bttsnfb *buildTaggedTBTCSignerNativeFROSTBridge) NewSigningPackage( +func (bttse *buildTaggedTBTCSignerEngine) StartSignRound( + sessionID string, message []byte, - commitments []uniFFINativeFROSTCommitment, -) (signingPackageData []byte, err error) { - return nil, buildTaggedTBTCSignerBridgeNotImplementedError("NewSigningPackage") -} - -func (bttsnfb *buildTaggedTBTCSignerNativeFROSTBridge) Sign( - signingPackageData []byte, - noncesData []byte, - keyPackageIdentifier string, - keyPackageData []byte, -) (signatureShareIdentifier string, signatureShareData []byte, err error) { - return "", nil, buildTaggedTBTCSignerBridgeNotImplementedError("Sign") + keyGroup string, +) (*NativeTBTCSignerRoundState, error) { + return nil, buildTaggedTBTCSignerBridgeNotImplementedError("StartSignRound") } -func (bttsnfb *buildTaggedTBTCSignerNativeFROSTBridge) Aggregate( - signingPackageData []byte, - signatureShares []uniFFINativeFROSTSignatureShare, - publicKeyPackage *NativeFROSTPublicKeyPackage, -) (signature []byte, err error) { - return nil, buildTaggedTBTCSignerBridgeNotImplementedError("Aggregate") +func (bttse *buildTaggedTBTCSignerEngine) FinalizeSignRound( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, +) ([]byte, error) { + return nil, buildTaggedTBTCSignerBridgeNotImplementedError("FinalizeSignRound") } func buildTaggedTBTCSignerBridgeNotImplementedError(operation string) error { diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index d0121835e6..7731adae64 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -7,10 +7,10 @@ import ( "testing" ) -func TestRegisterBuildTaggedTBTCSignerNativeFROSTSigningEngine(t *testing.T) { - UnregisterNativeFROSTSigningEngine() +func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { + UnregisterNativeTBTCSignerEngine() t.Cleanup(func() { - UnregisterNativeFROSTSigningEngine() + UnregisterNativeTBTCSignerEngine() }) err := registerBuildTaggedNativeFROSTSigningEngine() @@ -18,15 +18,16 @@ func TestRegisterBuildTaggedTBTCSignerNativeFROSTSigningEngine(t *testing.T) { t.Fatalf("unexpected registration error: [%v]", err) } - engine := currentNativeFROSTSigningEngine() + engine := currentNativeTBTCSignerEngine() if engine == nil { - t.Fatal("expected native FROST signing engine registration") + t.Fatal("expected native tbtc-signer engine registration") } - _, _, err = engine.GenerateNoncesAndCommitments(&NativeFROSTKeyPackage{ - Identifier: "participant-1", - Data: []byte{1, 2, 3}, - }) + _, err = engine.StartSignRound( + "session-1", + []byte("message"), + "key-group", + ) if err == nil { t.Fatal("expected not-implemented tbtc-signer bridge error") } diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go new file mode 100644 index 0000000000..6586bad4ca --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -0,0 +1,74 @@ +//go:build frost_native + +package signing + +import "fmt" + +const ( + // NativeSignerMaterialFormatFrostTBTCSignerV1 carries signer material for + // tbtc-signer coarse session APIs. + NativeSignerMaterialFormatFrostTBTCSignerV1 = "frost-tbtc-signer-v1" +) + +// NativeTBTCSignerRoundContribution is a participant contribution consumed by +// tbtc-signer during signature finalization. +type NativeTBTCSignerRoundContribution struct { + Identifier string `json:"identifier"` + Data []byte `json:"data"` +} + +// NativeTBTCSignerRoundState captures coarse session round metadata returned by +// StartSignRound. +type NativeTBTCSignerRoundState struct { + SessionID string `json:"sessionID"` + RoundID string `json:"roundID"` + RequiredContributions uint16 `json:"requiredContributions"` + MessageDigestHex string `json:"messageDigestHex"` +} + +// NativeTBTCSignerEngine executes coarse, session-keyed tbtc-signer +// operations. +type NativeTBTCSignerEngine interface { + StartSignRound( + sessionID string, + message []byte, + keyGroup string, + ) (*NativeTBTCSignerRoundState, error) + FinalizeSignRound( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, + ) ([]byte, error) +} + +var nativeTBTCSignerEngine NativeTBTCSignerEngine + +// RegisterNativeTBTCSignerEngine registers the coarse tbtc-signer engine used +// by frost_tbtc_signer builds. +func RegisterNativeTBTCSignerEngine(engine NativeTBTCSignerEngine) error { + if engine == nil { + return fmt.Errorf("native tbtc-signer engine is nil") + } + + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeTBTCSignerEngine = engine + + return nil +} + +// UnregisterNativeTBTCSignerEngine clears coarse tbtc-signer engine +// registration. +func UnregisterNativeTBTCSignerEngine() { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeTBTCSignerEngine = nil +} + +func currentNativeTBTCSignerEngine() NativeTBTCSignerEngine { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return nativeTBTCSignerEngine +} diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go new file mode 100644 index 0000000000..4cf55734ff --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go @@ -0,0 +1,66 @@ +//go:build frost_native + +package signing + +import ( + "fmt" + "testing" +) + +type mockNativeTBTCSignerEngine struct{} + +func (mntse *mockNativeTBTCSignerEngine) StartSignRound( + sessionID string, + message []byte, + keyGroup string, +) (*NativeTBTCSignerRoundState, error) { + return nil, fmt.Errorf("not implemented") +} + +func (mntse *mockNativeTBTCSignerEngine) FinalizeSignRound( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, +) ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +func TestRegisterNativeTBTCSignerEngineRejectsNil(t *testing.T) { + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + err := RegisterNativeTBTCSignerEngine(nil) + if err == nil { + t.Fatal("expected registration error") + } +} + +func TestRegisterNativeTBTCSignerEngine(t *testing.T) { + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + engine := &mockNativeTBTCSignerEngine{} + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + if currentNativeTBTCSignerEngine() != engine { + t.Fatal("expected current native tbtc-signer engine to match registered engine") + } +} + +func TestUnregisterNativeTBTCSignerEngine(t *testing.T) { + UnregisterNativeTBTCSignerEngine() + + err := RegisterNativeTBTCSignerEngine(&mockNativeTBTCSignerEngine{}) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + UnregisterNativeTBTCSignerEngine() + + if currentNativeTBTCSignerEngine() != nil { + t.Fatal("expected native tbtc-signer engine to be nil after unregister") + } +} From abe32f96656026f32464df42ab7b1b5bf353abea Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 12:35:17 -0600 Subject: [PATCH 44/96] Add frost_tbtc_signer material payload and legacy fallback shim --- ...ffi_primitive_transitional_frost_native.go | 141 +++++++++++++++--- ...rimitive_transitional_frost_native_test.go | 106 +++++++++++++ pkg/tbtc/signer_material_encoding.go | 40 ++++- ...ner_material_encoding_frost_native_test.go | 55 +++++-- pkg/tbtc/signer_material_payload.go | 8 + ...er_material_resolver_build_frost_native.go | 5 +- ...resolver_build_frost_native_tbtc_signer.go | 73 +++++++++ ...terial_resolver_build_frost_native_test.go | 61 ++++++-- 8 files changed, 430 insertions(+), 59 deletions(-) create mode 100644 pkg/tbtc/signer_material_payload.go create mode 100644 pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index cd2944fee1..1d89848408 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -4,6 +4,7 @@ package signing import ( "context" + "encoding/hex" "encoding/json" "fmt" @@ -90,37 +91,51 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) logger log.StandardLogger, request *NativeExecutionFFISigningRequest, ) (*frost.Signature, error) { - keyGroup, err := decodeBuildTaggedTBTCSignerKeyGroup(request.SignerMaterial) + payload, err := decodeBuildTaggedTBTCSignerMaterialPayload(request.SignerMaterial) + if err != nil { + return nil, err + } + + legacyPrivateKeyShare, err := decodeBuildTaggedTBTCSignerLegacyPrivateKeyShare(payload) if err != nil { return nil, err } engine := currentNativeTBTCSignerEngine() if engine == nil { - return nil, fmt.Errorf( - "%w: native tbtc-signer engine is unavailable", - ErrNativeCryptographyUnavailable, + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "native tbtc-signer engine is unavailable", ) } _, err = engine.StartSignRound( request.SessionID, request.Message.Bytes(), - keyGroup, + payload.KeyGroup, ) if err != nil { - return nil, fmt.Errorf( - "%w: tbtc-signer StartSignRound failed: [%v]", - ErrNativeCryptographyUnavailable, - err, + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf("tbtc-signer StartSignRound failed: [%v]", err), ) } // The coarse-session finalize flow is intentionally deferred until keep-core - // transport/orchestration is migrated from round-level message exchange. - return nil, fmt.Errorf( - "%w: tbtc-signer coarse session finalize flow is not wired", - ErrNativeCryptographyUnavailable, + // transport/orchestration is migrated from round-level message exchange. Use + // a Go-side legacy fallback while this migration is in progress. + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "tbtc-signer coarse session finalize flow is not wired", ) } @@ -136,6 +151,24 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) return nil, err } + return btlcnnefsp.signWithLegacyPrivateKeyShare( + ctx, + logger, + request, + privateKeyShare, + ) +} + +func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithLegacyPrivateKeyShare( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, + privateKeyShare *tecdsa.PrivateKeyShare, +) (*frost.Signature, error) { + if privateKeyShare == nil { + return nil, fmt.Errorf("legacy private key share is nil") + } + excludedMembersIndexes := []group.MemberIndex{} if request.Attempt != nil { excludedMembersIndexes = request.Attempt.ExcludedMembersIndexes @@ -161,6 +194,32 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) return FromTECDSASignature(legacyResult.Signature) } +func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) fallbackTBTCSignerLegacySigning( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, + legacyPrivateKeyShare *tecdsa.PrivateKeyShare, + reason string, +) (*frost.Signature, error) { + if legacyPrivateKeyShare == nil { + return nil, fmt.Errorf("%w: %s", ErrNativeCryptographyUnavailable, reason) + } + + if logger != nil { + logger.Warnf( + "falling back to legacy tECDSA signer path for tbtc-signer payload: [%s]", + reason, + ) + } + + return btlcnnefsp.signWithLegacyPrivateKeyShare( + ctx, + logger, + request, + legacyPrivateKeyShare, + ) +} + func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( channel net.BroadcastChannel, ) { @@ -206,21 +265,22 @@ func decodeBuildTaggedLegacyPrivateKeyShare( } type buildTaggedTBTCSignerMaterialPayload struct { - KeyGroup string `json:"keyGroup"` + KeyGroup string `json:"keyGroup"` + LegacyPrivateKeyShareHex string `json:"legacyPrivateKeyShareHex,omitempty"` } -func decodeBuildTaggedTBTCSignerKeyGroup( +func decodeBuildTaggedTBTCSignerMaterialPayload( signerMaterial *NativeSignerMaterial, -) (string, error) { +) (*buildTaggedTBTCSignerMaterialPayload, error) { if signerMaterial == nil { - return "", fmt.Errorf( + return nil, fmt.Errorf( "%w: signer material is nil", ErrNativeCryptographyUnavailable, ) } if signerMaterial.Format != NativeSignerMaterialFormatFrostTBTCSignerV1 { - return "", fmt.Errorf( + return nil, fmt.Errorf( "%w: unsupported signer material format: [%s]", ErrNativeCryptographyUnavailable, signerMaterial.Format, @@ -228,7 +288,7 @@ func decodeBuildTaggedTBTCSignerKeyGroup( } if len(signerMaterial.Payload) == 0 { - return "", fmt.Errorf( + return nil, fmt.Errorf( "%w: signer material payload is empty", ErrNativeCryptographyUnavailable, ) @@ -236,7 +296,7 @@ func decodeBuildTaggedTBTCSignerKeyGroup( var payload buildTaggedTBTCSignerMaterialPayload if err := json.Unmarshal(signerMaterial.Payload, &payload); err != nil { - return "", fmt.Errorf( + return nil, fmt.Errorf( "%w: cannot unmarshal tbtc-signer payload: [%v]", ErrNativeCryptographyUnavailable, err, @@ -244,11 +304,50 @@ func decodeBuildTaggedTBTCSignerKeyGroup( } if payload.KeyGroup == "" { - return "", fmt.Errorf( + return nil, fmt.Errorf( "%w: tbtc-signer key group is empty", ErrNativeCryptographyUnavailable, ) } + return &payload, nil +} + +func decodeBuildTaggedTBTCSignerKeyGroup( + signerMaterial *NativeSignerMaterial, +) (string, error) { + payload, err := decodeBuildTaggedTBTCSignerMaterialPayload(signerMaterial) + if err != nil { + return "", err + } + return payload.KeyGroup, nil } + +func decodeBuildTaggedTBTCSignerLegacyPrivateKeyShare( + payload *buildTaggedTBTCSignerMaterialPayload, +) (*tecdsa.PrivateKeyShare, error) { + if payload == nil || payload.LegacyPrivateKeyShareHex == "" { + return nil, nil + } + + legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) + if err != nil { + return nil, fmt.Errorf( + "%w: cannot decode tbtc-signer legacy private key share: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + privateKeyShare := &tecdsa.PrivateKeyShare{} + if err := privateKeyShare.Unmarshal(legacyPrivateKeySharePayload); err != nil { + return nil, fmt.Errorf( + "%w: cannot unmarshal tbtc-signer legacy private key share: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + return privateKeyShare, nil +} diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 454a2601e8..fb41aeca70 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -4,6 +4,7 @@ package signing import ( "bytes" + "encoding/hex" "errors" "fmt" "math/big" @@ -251,6 +252,111 @@ func TestDecodeBuildTaggedTBTCSignerKeyGroup_RejectsInvalidMaterial( } } +func TestDecodeBuildTaggedTBTCSignerLegacyPrivateKeyShare(t *testing.T) { + fixtures, err := tecdsatest.LoadPrivateKeyShareTestFixtures(5) + if err != nil { + t.Fatalf("failed loading key share fixtures: [%v]", err) + } + + expectedPrivateKeyShare := tecdsa.NewPrivateKeyShare(fixtures[0]) + expectedPayload, err := expectedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling private key share: [%v]", err) + } + + decodedPrivateKeyShare, err := decodeBuildTaggedTBTCSignerLegacyPrivateKeyShare( + &buildTaggedTBTCSignerMaterialPayload{ + KeyGroup: "group-1", + LegacyPrivateKeyShareHex: hex.EncodeToString(expectedPayload), + }, + ) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + if decodedPrivateKeyShare == nil { + t.Fatal("expected decoded private key share") + } + + actualPayload, err := decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + + if !bytes.Equal(expectedPayload, actualPayload) { + t.Fatalf( + "unexpected decoded private key share\nexpected: [%x]\nactual: [%x]", + expectedPayload, + actualPayload, + ) + } +} + +func TestDecodeBuildTaggedTBTCSignerLegacyPrivateKeyShare_RejectsInvalidPayload( + t *testing.T, +) { + testCases := []struct { + name string + payload *buildTaggedTBTCSignerMaterialPayload + expectError bool + }{ + { + name: "nil payload", + payload: nil, + expectError: false, + }, + { + name: "empty legacy private key share", + payload: &buildTaggedTBTCSignerMaterialPayload{}, + expectError: false, + }, + { + name: "invalid hex", + payload: &buildTaggedTBTCSignerMaterialPayload{ + LegacyPrivateKeyShareHex: "zz", + }, + expectError: true, + }, + { + name: "invalid private key share payload", + payload: &buildTaggedTBTCSignerMaterialPayload{ + LegacyPrivateKeyShareHex: hex.EncodeToString(big.NewInt(123).Bytes()), + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + decoded, err := decodeBuildTaggedTBTCSignerLegacyPrivateKeyShare(tc.payload) + + if tc.expectError { + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + return + } + + if err != nil { + t.Fatalf("expected nil error, got: [%v]", err) + } + + if decoded != nil { + t.Fatalf("expected nil decoded private key share, got: [%v]", decoded) + } + }) + } +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath( t *testing.T, ) { diff --git a/pkg/tbtc/signer_material_encoding.go b/pkg/tbtc/signer_material_encoding.go index 4b13f5e492..662d4eed0e 100644 --- a/pkg/tbtc/signer_material_encoding.go +++ b/pkg/tbtc/signer_material_encoding.go @@ -3,6 +3,8 @@ package tbtc import ( "bytes" "encoding/binary" + "encoding/hex" + "encoding/json" "fmt" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" @@ -232,14 +234,38 @@ func legacyPrivateKeyShareFromNativeSignerMaterial( return nil } - if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { - return nil - } + switch nativeSignerMaterial.Format { + case frostsigning.NativeSignerMaterialFormatFrostUniFFIV1: + privateKeyShare := &tecdsa.PrivateKeyShare{} + if err := privateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { + return nil + } - privateKeyShare := &tecdsa.PrivateKeyShare{} - if err := privateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { + return privateKeyShare + + case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: + var payload tbtcSignerMaterialPayload + if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { + return nil + } + + if payload.LegacyPrivateKeyShareHex == "" { + return nil + } + + legacyPayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) + if err != nil { + return nil + } + + privateKeyShare := &tecdsa.PrivateKeyShare{} + if err := privateKeyShare.Unmarshal(legacyPayload); err != nil { + return nil + } + + return privateKeyShare + + default: return nil } - - return privateKeyShare } diff --git a/pkg/tbtc/signer_material_encoding_frost_native_test.go b/pkg/tbtc/signer_material_encoding_frost_native_test.go index 9d624b1807..9b98e2cfbc 100644 --- a/pkg/tbtc/signer_material_encoding_frost_native_test.go +++ b/pkg/tbtc/signer_material_encoding_frost_native_test.go @@ -4,6 +4,8 @@ package tbtc import ( "bytes" + "encoding/hex" + "encoding/json" "testing" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" @@ -50,24 +52,51 @@ func TestUnmarshalSignerMaterialFromPersistence_LegacyEncodingResolvesNativeMate ) } - if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { + var actualPayload []byte + switch nativeSignerMaterial.Format { + case frostsigning.NativeSignerMaterialFormatFrostUniFFIV1: + decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} + if err := decodedPrivateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { + t.Fatalf("failed unmarshalling native signer material payload: [%v]", err) + } + + actualPayload, err = decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + + case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: + var payload tbtcSignerMaterialPayload + if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { + t.Fatalf("failed unmarshalling tbtc signer material payload: [%v]", err) + } + + if payload.KeyGroup == "" { + t.Fatal("expected non-empty tbtc-signer key group") + } + + legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) + if err != nil { + t.Fatalf("failed decoding legacy private key share payload: [%v]", err) + } + + decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} + if err := decodedPrivateKeyShare.Unmarshal(legacyPrivateKeySharePayload); err != nil { + t.Fatalf("failed unmarshalling decoded private key share: [%v]", err) + } + + actualPayload, err = decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + + default: t.Fatalf( - "unexpected signer material format\nexpected: [%v]\nactual: [%v]", - frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + "unexpected signer material format\nactual: [%v]", nativeSignerMaterial.Format, ) } - decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} - if err := decodedPrivateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { - t.Fatalf("failed unmarshalling native signer material payload: [%v]", err) - } - - actualPayload, err := decodedPrivateKeyShare.Marshal() - if err != nil { - t.Fatalf("failed marshaling decoded private key share: [%v]", err) - } - if !bytes.Equal(actualPayload, legacyEncoded) { t.Fatalf( "unexpected resolved signer payload\nexpected: [%x]\nactual: [%x]", diff --git a/pkg/tbtc/signer_material_payload.go b/pkg/tbtc/signer_material_payload.go new file mode 100644 index 0000000000..00c9af1048 --- /dev/null +++ b/pkg/tbtc/signer_material_payload.go @@ -0,0 +1,8 @@ +package tbtc + +// tbtcSignerMaterialPayload is the persisted signer-material payload for +// `frost-tbtc-signer-v1`. +type tbtcSignerMaterialPayload struct { + KeyGroup string `json:"keyGroup"` + LegacyPrivateKeyShareHex string `json:"legacyPrivateKeyShareHex,omitempty"` +} diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native.go b/pkg/tbtc/signer_material_resolver_build_frost_native.go index fa78d1c1e3..dca4c73848 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native.go @@ -1,4 +1,4 @@ -//go:build frost_native +//go:build frost_native && !(frost_tbtc_signer && cgo) package tbtc @@ -32,7 +32,8 @@ func defaultSignerMaterialResolverProviderForBuild() (SignerMaterialResolver, er } // buildTaggedNativeSignerMaterialResolver derives transitional native signer -// material from a legacy private key share for frost_native builds. +// material from a legacy private key share for frost_native builds not using +// the `frost_tbtc_signer` tag. type buildTaggedNativeSignerMaterialResolver struct{} func (btnsmr *buildTaggedNativeSignerMaterialResolver) ResolveSignerMaterial( diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go b/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go new file mode 100644 index 0000000000..f843fab186 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go @@ -0,0 +1,73 @@ +//go:build frost_native && frost_tbtc_signer && cgo + +package tbtc + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func registerSignerMaterialResolverForBuild() error { + provider := currentSignerMaterialResolverProviderForBuild() + if provider == nil { + provider = defaultSignerMaterialResolverProviderForBuild + } + + resolver, err := provider() + if err != nil { + return err + } + + if resolver == nil { + return fmt.Errorf("signer material resolver is nil") + } + + return RegisterSignerMaterialResolver(resolver) +} + +func defaultSignerMaterialResolverProviderForBuild() (SignerMaterialResolver, error) { + return &buildTaggedNativeSignerMaterialResolver{}, nil +} + +// buildTaggedNativeSignerMaterialResolver derives transitional signer material +// for frost_tbtc_signer builds. It carries a deterministic key-group handle and +// embeds legacy private-key-share bytes to preserve temporary Go-side fallback. +type buildTaggedNativeSignerMaterialResolver struct{} + +func (btnsmr *buildTaggedNativeSignerMaterialResolver) ResolveSignerMaterial( + privateKeyShare *tecdsa.PrivateKeyShare, +) (any, error) { + if privateKeyShare == nil { + return nil, fmt.Errorf("private key share is nil") + } + + legacyPrivateKeySharePayload, err := privateKeyShare.Marshal() + if err != nil { + return nil, fmt.Errorf("cannot marshal private key share: [%w]", err) + } + + walletPublicKeyBytes, err := marshalPublicKey(privateKeyShare.PublicKey()) + if err != nil { + return nil, fmt.Errorf("cannot marshal wallet public key: [%w]", err) + } + + keyGroupDigest := sha256.Sum256(walletPublicKeyBytes) + + payload, err := json.Marshal(tbtcSignerMaterialPayload{ + KeyGroup: hex.EncodeToString(keyGroupDigest[:]), + LegacyPrivateKeyShareHex: hex.EncodeToString(legacyPrivateKeySharePayload), + }) + if err != nil { + return nil, fmt.Errorf("cannot marshal tbtc signer material payload: [%w]", err) + } + + return &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + }, nil +} diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go index 886745464f..ef351d1c4a 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go @@ -4,6 +4,8 @@ package tbtc import ( "bytes" + "encoding/hex" + "encoding/json" "errors" "testing" @@ -40,27 +42,54 @@ func TestRegisterSignerMaterialResolverForBuild_UsesDefaultProvider( ) } - if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV1 { - t.Fatalf( - "unexpected native signer material format\nexpected: [%s]\nactual: [%s]", - frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, - nativeSignerMaterial.Format, - ) - } - - decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} - if err := decodedPrivateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { - t.Fatalf("failed unmarshalling resolved signer payload: [%v]", err) - } - expectedPayload, err := privateKeyShare.Marshal() if err != nil { t.Fatalf("failed marshaling expected private key share: [%v]", err) } - actualPayload, err := decodedPrivateKeyShare.Marshal() - if err != nil { - t.Fatalf("failed marshaling decoded private key share: [%v]", err) + var actualPayload []byte + switch nativeSignerMaterial.Format { + case frostsigning.NativeSignerMaterialFormatFrostUniFFIV1: + decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} + if err := decodedPrivateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { + t.Fatalf("failed unmarshalling resolved signer payload: [%v]", err) + } + + actualPayload, err = decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + + case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: + var payload tbtcSignerMaterialPayload + if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { + t.Fatalf("failed unmarshalling tbtc signer material payload: [%v]", err) + } + + if payload.KeyGroup == "" { + t.Fatal("expected non-empty tbtc-signer key group") + } + + legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) + if err != nil { + t.Fatalf("failed decoding legacy private key share payload: [%v]", err) + } + + decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} + if err := decodedPrivateKeyShare.Unmarshal(legacyPrivateKeySharePayload); err != nil { + t.Fatalf("failed unmarshalling decoded private key share: [%v]", err) + } + + actualPayload, err = decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + + default: + t.Fatalf( + "unexpected native signer material format: [%s]", + nativeSignerMaterial.Format, + ) } if !bytes.Equal(expectedPayload, actualPayload) { From 7d7432610b62ff990afc7f0f4dae316b23ba16c6 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 12:51:14 -0600 Subject: [PATCH 45/96] Address Claude review blockers in tbtc-signer scaffold --- ...ffi_primitive_transitional_frost_native.go | 55 ++++++------------- ...rimitive_transitional_frost_native_test.go | 48 ++++++++++------ .../native_tbtc_signer_engine_frost_native.go | 10 +++- pkg/tbtc/signer_material_encoding.go | 2 +- ...ner_material_encoding_frost_native_test.go | 6 +- pkg/tbtc/signer_material_payload.go | 8 --- ...resolver_build_frost_native_tbtc_signer.go | 5 +- ...terial_resolver_build_frost_native_test.go | 6 +- 8 files changed, 73 insertions(+), 67 deletions(-) delete mode 100644 pkg/tbtc/signer_material_payload.go diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 1d89848408..d6a6a99f8f 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -30,8 +30,9 @@ func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( // buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive is a // transitional primitive that executes native two-round FROST when // `frost-uniffi-v2` signer material is provided, and preserves legacy bridge -// execution for `frost-uniffi-v1` payloads. `frost-tbtc-signer-v1` is reserved -// for coarse session engine integration and currently returns a scaffold error. +// execution for `frost-uniffi-v1` payloads. `frost-tbtc-signer-v1` currently +// routes through a temporary legacy fallback until coarse session finalize flow +// is wired end-to-end. type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) Sign( @@ -101,41 +102,26 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) return nil, err } - engine := currentNativeTBTCSignerEngine() - if engine == nil { - return btlcnnefsp.fallbackTBTCSignerLegacySigning( - ctx, - logger, - request, - legacyPrivateKeyShare, - "native tbtc-signer engine is unavailable", - ) - } - - _, err = engine.StartSignRound( - request.SessionID, - request.Message.Bytes(), - payload.KeyGroup, - ) - if err != nil { - return btlcnnefsp.fallbackTBTCSignerLegacySigning( - ctx, - logger, - request, - legacyPrivateKeyShare, - fmt.Sprintf("tbtc-signer StartSignRound failed: [%v]", err), + // Do not start coarse native sessions until finalize flow is wired. Calling + // StartSignRound without finalize would orphan signer-engine state. + if currentNativeTBTCSignerEngine() != nil && logger != nil { + logger.Warnf( + "native tbtc-signer engine is registered but coarse finalize flow is not wired; using legacy fallback", ) } - // The coarse-session finalize flow is intentionally deferred until keep-core - // transport/orchestration is migrated from round-level message exchange. Use - // a Go-side legacy fallback while this migration is in progress. + // The coarse-session flow is intentionally deferred until keep-core + // orchestration is migrated from round-level message exchange. Use a Go-side + // legacy fallback while this migration is in progress. return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, logger, request, legacyPrivateKeyShare, - "tbtc-signer coarse session finalize flow is not wired", + fmt.Sprintf( + "tbtc-signer coarse session flow is not wired (keyGroupSource=%s)", + payload.KeyGroupSource, + ), ) } @@ -264,14 +250,9 @@ func decodeBuildTaggedLegacyPrivateKeyShare( return privateKeyShare, nil } -type buildTaggedTBTCSignerMaterialPayload struct { - KeyGroup string `json:"keyGroup"` - LegacyPrivateKeyShareHex string `json:"legacyPrivateKeyShareHex,omitempty"` -} - func decodeBuildTaggedTBTCSignerMaterialPayload( signerMaterial *NativeSignerMaterial, -) (*buildTaggedTBTCSignerMaterialPayload, error) { +) (*NativeTBTCSignerMaterialPayload, error) { if signerMaterial == nil { return nil, fmt.Errorf( "%w: signer material is nil", @@ -294,7 +275,7 @@ func decodeBuildTaggedTBTCSignerMaterialPayload( ) } - var payload buildTaggedTBTCSignerMaterialPayload + var payload NativeTBTCSignerMaterialPayload if err := json.Unmarshal(signerMaterial.Payload, &payload); err != nil { return nil, fmt.Errorf( "%w: cannot unmarshal tbtc-signer payload: [%v]", @@ -325,7 +306,7 @@ func decodeBuildTaggedTBTCSignerKeyGroup( } func decodeBuildTaggedTBTCSignerLegacyPrivateKeyShare( - payload *buildTaggedTBTCSignerMaterialPayload, + payload *NativeTBTCSignerMaterialPayload, ) (*tecdsa.PrivateKeyShare, error) { if payload == nil || payload.LegacyPrivateKeyShareHex == "" { return nil, nil diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index fb41aeca70..2feffaf883 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -265,7 +265,7 @@ func TestDecodeBuildTaggedTBTCSignerLegacyPrivateKeyShare(t *testing.T) { } decodedPrivateKeyShare, err := decodeBuildTaggedTBTCSignerLegacyPrivateKeyShare( - &buildTaggedTBTCSignerMaterialPayload{ + &NativeTBTCSignerMaterialPayload{ KeyGroup: "group-1", LegacyPrivateKeyShareHex: hex.EncodeToString(expectedPayload), }, @@ -297,7 +297,7 @@ func TestDecodeBuildTaggedTBTCSignerLegacyPrivateKeyShare_RejectsInvalidPayload( ) { testCases := []struct { name string - payload *buildTaggedTBTCSignerMaterialPayload + payload *NativeTBTCSignerMaterialPayload expectError bool }{ { @@ -307,19 +307,19 @@ func TestDecodeBuildTaggedTBTCSignerLegacyPrivateKeyShare_RejectsInvalidPayload( }, { name: "empty legacy private key share", - payload: &buildTaggedTBTCSignerMaterialPayload{}, + payload: &NativeTBTCSignerMaterialPayload{}, expectError: false, }, { name: "invalid hex", - payload: &buildTaggedTBTCSignerMaterialPayload{ + payload: &NativeTBTCSignerMaterialPayload{ LegacyPrivateKeyShareHex: "zz", }, expectError: true, }, { name: "invalid private key share payload", - payload: &buildTaggedTBTCSignerMaterialPayload{ + payload: &NativeTBTCSignerMaterialPayload{ LegacyPrivateKeyShareHex: hex.EncodeToString(big.NewInt(123).Bytes()), }, expectError: true, @@ -391,23 +391,37 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } - if !engine.startCalled { - t.Fatal("expected StartSignRound call") + if engine.startCalled { + t.Fatal("did not expect StartSignRound call while coarse finalize flow is unwired") } - if engine.sessionID != "session-1" { - t.Fatalf( - "unexpected session ID\nexpected: [%v]\nactual: [%v]", - "session-1", - engine.sessionID, - ) +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_NoEngineNoLegacyShare( + t *testing.T, +) { + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":"group-1"}`), + }, + }) + if err == nil { + t.Fatal("expected error") } - if engine.keyGroup != "group-1" { + if !errors.Is(err, ErrNativeCryptographyUnavailable) { t.Fatalf( - "unexpected key group\nexpected: [%v]\nactual: [%v]", - "group-1", - engine.keyGroup, + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, ) } } diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go index 6586bad4ca..39c5dc3bd0 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -10,10 +10,18 @@ const ( NativeSignerMaterialFormatFrostTBTCSignerV1 = "frost-tbtc-signer-v1" ) +// NativeTBTCSignerMaterialPayload is the signer-material payload schema for +// `frost-tbtc-signer-v1`. +type NativeTBTCSignerMaterialPayload struct { + KeyGroup string `json:"keyGroup"` + KeyGroupSource string `json:"keyGroupSource,omitempty"` + LegacyPrivateKeyShareHex string `json:"legacyPrivateKeyShareHex,omitempty"` +} + // NativeTBTCSignerRoundContribution is a participant contribution consumed by // tbtc-signer during signature finalization. type NativeTBTCSignerRoundContribution struct { - Identifier string `json:"identifier"` + Identifier uint16 `json:"identifier"` Data []byte `json:"data"` } diff --git a/pkg/tbtc/signer_material_encoding.go b/pkg/tbtc/signer_material_encoding.go index 662d4eed0e..c4a416abbc 100644 --- a/pkg/tbtc/signer_material_encoding.go +++ b/pkg/tbtc/signer_material_encoding.go @@ -244,7 +244,7 @@ func legacyPrivateKeyShareFromNativeSignerMaterial( return privateKeyShare case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: - var payload tbtcSignerMaterialPayload + var payload frostsigning.NativeTBTCSignerMaterialPayload if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { return nil } diff --git a/pkg/tbtc/signer_material_encoding_frost_native_test.go b/pkg/tbtc/signer_material_encoding_frost_native_test.go index 9b98e2cfbc..324e854bcd 100644 --- a/pkg/tbtc/signer_material_encoding_frost_native_test.go +++ b/pkg/tbtc/signer_material_encoding_frost_native_test.go @@ -66,7 +66,7 @@ func TestUnmarshalSignerMaterialFromPersistence_LegacyEncodingResolvesNativeMate } case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: - var payload tbtcSignerMaterialPayload + var payload frostsigning.NativeTBTCSignerMaterialPayload if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { t.Fatalf("failed unmarshalling tbtc signer material payload: [%v]", err) } @@ -75,6 +75,10 @@ func TestUnmarshalSignerMaterialFromPersistence_LegacyEncodingResolvesNativeMate t.Fatal("expected non-empty tbtc-signer key group") } + if payload.KeyGroupSource == "" { + t.Fatal("expected non-empty tbtc-signer key group source") + } + legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) if err != nil { t.Fatalf("failed decoding legacy private key share payload: [%v]", err) diff --git a/pkg/tbtc/signer_material_payload.go b/pkg/tbtc/signer_material_payload.go deleted file mode 100644 index 00c9af1048..0000000000 --- a/pkg/tbtc/signer_material_payload.go +++ /dev/null @@ -1,8 +0,0 @@ -package tbtc - -// tbtcSignerMaterialPayload is the persisted signer-material payload for -// `frost-tbtc-signer-v1`. -type tbtcSignerMaterialPayload struct { - KeyGroup string `json:"keyGroup"` - LegacyPrivateKeyShareHex string `json:"legacyPrivateKeyShareHex,omitempty"` -} diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go b/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go index f843fab186..a7e6e81772 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go @@ -58,8 +58,11 @@ func (btnsmr *buildTaggedNativeSignerMaterialResolver) ResolveSignerMaterial( keyGroupDigest := sha256.Sum256(walletPublicKeyBytes) - payload, err := json.Marshal(tbtcSignerMaterialPayload{ + // TODO: Replace this placeholder key-group derivation with Rust DKG output. + // The current value identifies scaffold-era material only. + payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ KeyGroup: hex.EncodeToString(keyGroupDigest[:]), + KeyGroupSource: "legacy-wallet-pubkey", LegacyPrivateKeyShareHex: hex.EncodeToString(legacyPrivateKeySharePayload), }) if err != nil { diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go index ef351d1c4a..4138dc0894 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go @@ -61,7 +61,7 @@ func TestRegisterSignerMaterialResolverForBuild_UsesDefaultProvider( } case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: - var payload tbtcSignerMaterialPayload + var payload frostsigning.NativeTBTCSignerMaterialPayload if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { t.Fatalf("failed unmarshalling tbtc signer material payload: [%v]", err) } @@ -70,6 +70,10 @@ func TestRegisterSignerMaterialResolverForBuild_UsesDefaultProvider( t.Fatal("expected non-empty tbtc-signer key group") } + if payload.KeyGroupSource == "" { + t.Fatal("expected non-empty tbtc-signer key group source") + } + legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) if err != nil { t.Fatalf("failed decoding legacy private key share payload: [%v]", err) From e837a32c65380471de611c79b971b9803b897ed1 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 13:16:24 -0600 Subject: [PATCH 46/96] Add tbtc-signer fallback telemetry and metric wiring --- pkg/clientinfo/performance.go | 78 +++++++++++++------ pkg/clientinfo/performance_test.go | 1 + ...ffi_primitive_transitional_frost_native.go | 11 +++ ...rimitive_transitional_frost_native_test.go | 45 ++++++++++- .../native_tbtc_signer_fallback_telemetry.go | 61 +++++++++++++++ ...ive_tbtc_signer_fallback_telemetry_test.go | 56 +++++++++++++ pkg/tbtc/node.go | 20 +++++ 7 files changed, 246 insertions(+), 26 deletions(-) create mode 100644 pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go create mode 100644 pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go diff --git a/pkg/clientinfo/performance.go b/pkg/clientinfo/performance.go index bed76c1019..30f886508e 100644 --- a/pkg/clientinfo/performance.go +++ b/pkg/clientinfo/performance.go @@ -107,6 +107,7 @@ func (pm *PerformanceMetrics) registerAllMetrics() { MetricSigningSuccessTotal, MetricSigningFailedTotal, MetricSigningTimeoutsTotal, + MetricSigningNativeTBTCSignerFallbackTotal, MetricWalletActionsTotal, MetricWalletActionSuccessTotal, MetricWalletActionFailedTotal, @@ -125,9 +126,11 @@ func (pm *PerformanceMetrics) registerAllMetrics() { } // First, initialize all counters in the map + pm.countersMutex.Lock() for _, name := range counters { pm.counters[name] = &counter{value: 0} } + pm.countersMutex.Unlock() // Then, register observers (this prevents concurrent map read/write) for _, name := range counters { @@ -151,39 +154,59 @@ func (pm *PerformanceMetrics) registerAllMetrics() { } // Register per-action type wallet metrics - // For each action type, register: total, success_total, failed_total, duration_seconds + // For each action type, register: total, success_total, failed_total, duration_seconds. + // Collect first, then initialize all maps, and only then register observers to + // avoid concurrent map writes while observers are reading. + perActionCounters := []string{} + perActionDurations := []string{} for _, actionType := range GetAllWalletActionTypes() { - actionCounters := []string{ + perActionCounters = append( + perActionCounters, WalletActionMetricName(actionType, "total"), WalletActionMetricName(actionType, "success_total"), WalletActionMetricName(actionType, "failed_total"), - } - for _, name := range actionCounters { - pm.counters[name] = &counter{value: 0} - metricName := name // Capture for closure - pm.registry.ObserveApplicationSource( - "performance", - map[string]Source{ - metricName: func() float64 { - pm.countersMutex.RLock() - c, exists := pm.counters[metricName] - pm.countersMutex.RUnlock() - if !exists { - return 0 - } - c.mutex.RLock() - defer c.mutex.RUnlock() - return c.value - }, + ) + perActionDurations = append( + perActionDurations, + WalletActionMetricName(actionType, "duration_seconds"), + ) + } + + pm.countersMutex.Lock() + for _, name := range perActionCounters { + pm.counters[name] = &counter{value: 0} + } + pm.countersMutex.Unlock() + + for _, name := range perActionCounters { + metricName := name // Capture for closure + pm.registry.ObserveApplicationSource( + "performance", + map[string]Source{ + metricName: func() float64 { + pm.countersMutex.RLock() + c, exists := pm.counters[metricName] + pm.countersMutex.RUnlock() + if !exists { + return 0 + } + c.mutex.RLock() + defer c.mutex.RUnlock() + return c.value }, - ) - } + }, + ) + } - // Register duration metric for this action type - durationName := WalletActionMetricName(actionType, "duration_seconds") + pm.histogramsMutex.Lock() + for _, durationName := range perActionDurations { pm.histograms[durationName] = &histogram{ buckets: make(map[float64]float64), } + } + pm.histogramsMutex.Unlock() + + for _, durationName := range perActionDurations { durationMetricName := durationName // Capture for closure pm.registry.ObserveApplicationSource( "performance", @@ -218,11 +241,13 @@ func (pm *PerformanceMetrics) registerAllMetrics() { } // First, initialize all histograms in the map + pm.histogramsMutex.Lock() for _, name := range durationMetrics { pm.histograms[name] = &histogram{ buckets: make(map[float64]float64), } } + pm.histogramsMutex.Unlock() // Then, register observers (this prevents concurrent map read/write) for _, name := range durationMetrics { @@ -273,9 +298,11 @@ func (pm *PerformanceMetrics) registerAllMetrics() { } // First, initialize all gauges in the map + pm.gaugesMutex.Lock() for _, name := range gauges { pm.gauges[name] = &gauge{value: 0} } + pm.gaugesMutex.Unlock() // Then, register observers (this prevents concurrent map read/write) for _, name := range gauges { @@ -549,6 +576,9 @@ const ( MetricSigningDurationSeconds = "signing_duration_seconds" MetricSigningAttemptsPerOperation = "signing_attempts_per_operation" MetricSigningTimeoutsTotal = "signing_timeouts_total" + // MetricSigningNativeTBTCSignerFallbackTotal counts the number of times the + // frost_tbtc_signer path fell back to legacy tECDSA execution. + MetricSigningNativeTBTCSignerFallbackTotal = "signing_native_tbtc_signer_fallback_total" // Wallet Action Metrics (aggregate) MetricWalletActionsTotal = "wallet_actions_total" diff --git a/pkg/clientinfo/performance_test.go b/pkg/clientinfo/performance_test.go index 86e7283c55..d9d89dae43 100644 --- a/pkg/clientinfo/performance_test.go +++ b/pkg/clientinfo/performance_test.go @@ -306,6 +306,7 @@ func TestMetricsInitialization(t *testing.T) { MetricDKGJoinedTotal, MetricSigningOperationsTotal, MetricSigningSuccessTotal, + MetricSigningNativeTBTCSignerFallbackTotal, } for _, counterName := range counters { diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index d6a6a99f8f..54c58fb589 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -122,6 +122,7 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) "tbtc-signer coarse session flow is not wired (keyGroupSource=%s)", payload.KeyGroupSource, ), + payload.KeyGroupSource, ) } @@ -186,7 +187,17 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) request *NativeExecutionFFISigningRequest, legacyPrivateKeyShare *tecdsa.PrivateKeyShare, reason string, + keyGroupSource string, ) (*frost.Signature, error) { + emitNativeTBTCSignerFallbackEvent( + NativeTBTCSignerFallbackEvent{ + SessionID: request.SessionID, + Reason: reason, + KeyGroupSource: keyGroupSource, + LegacyPrivateKeyShareExists: legacyPrivateKeyShare != nil, + }, + ) + if legacyPrivateKeyShare == nil { return nil, fmt.Errorf("%w: %s", ErrNativeCryptographyUnavailable, reason) } diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 2feffaf883..72c5e8115e 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -401,16 +401,28 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC t *testing.T, ) { UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + var observedEvents []NativeTBTCSignerFallbackEvent + err := RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + observedEvents = append(observedEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected observer registration error: [%v]", err) + } primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} - _, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ Message: big.NewInt(123), SessionID: "session-1", SignerMaterial: &NativeSignerMaterial{ Format: NativeSignerMaterialFormatFrostTBTCSignerV1, - Payload: []byte(`{"keyGroup":"group-1"}`), + Payload: []byte(`{"keyGroup":"group-1","keyGroupSource":"legacy-wallet-pubkey"}`), }, }) if err == nil { @@ -424,4 +436,33 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC err, ) } + + if len(observedEvents) != 1 { + t.Fatalf( + "unexpected fallback event count\nexpected: [%d]\nactual: [%d]", + 1, + len(observedEvents), + ) + } + + event := observedEvents[0] + if event.SessionID != "session-1" { + t.Fatalf( + "unexpected fallback session ID\nexpected: [%s]\nactual: [%s]", + "session-1", + event.SessionID, + ) + } + + if event.KeyGroupSource != "legacy-wallet-pubkey" { + t.Fatalf( + "unexpected fallback key group source\nexpected: [%s]\nactual: [%s]", + "legacy-wallet-pubkey", + event.KeyGroupSource, + ) + } + + if event.LegacyPrivateKeyShareExists { + t.Fatal("expected fallback event without legacy private key share") + } } diff --git a/pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go new file mode 100644 index 0000000000..09ee08054d --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go @@ -0,0 +1,61 @@ +package signing + +import ( + "fmt" + "sync" +) + +// NativeTBTCSignerFallbackEvent describes a single fallback from the +// tbtc-signer coarse path to the legacy signing path. +type NativeTBTCSignerFallbackEvent struct { + SessionID string + Reason string + KeyGroupSource string + LegacyPrivateKeyShareExists bool +} + +// NativeTBTCSignerFallbackObserver consumes fallback telemetry events. +type NativeTBTCSignerFallbackObserver func(event NativeTBTCSignerFallbackEvent) + +var ( + nativeTBTCSignerFallbackObserverMutex sync.RWMutex + nativeTBTCSignerFallbackObserver NativeTBTCSignerFallbackObserver +) + +// RegisterNativeTBTCSignerFallbackObserver registers a process-wide observer +// used to report tbtc-signer fallback events. +func RegisterNativeTBTCSignerFallbackObserver( + observer NativeTBTCSignerFallbackObserver, +) error { + if observer == nil { + return fmt.Errorf("native tbtc-signer fallback observer is nil") + } + + nativeTBTCSignerFallbackObserverMutex.Lock() + defer nativeTBTCSignerFallbackObserverMutex.Unlock() + + nativeTBTCSignerFallbackObserver = observer + + return nil +} + +// UnregisterNativeTBTCSignerFallbackObserver clears fallback-observer +// registration. +func UnregisterNativeTBTCSignerFallbackObserver() { + nativeTBTCSignerFallbackObserverMutex.Lock() + defer nativeTBTCSignerFallbackObserverMutex.Unlock() + + nativeTBTCSignerFallbackObserver = nil +} + +func emitNativeTBTCSignerFallbackEvent(event NativeTBTCSignerFallbackEvent) { + nativeTBTCSignerFallbackObserverMutex.RLock() + observer := nativeTBTCSignerFallbackObserver + nativeTBTCSignerFallbackObserverMutex.RUnlock() + + if observer == nil { + return + } + + observer(event) +} diff --git a/pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go new file mode 100644 index 0000000000..45d8039bf2 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go @@ -0,0 +1,56 @@ +package signing + +import ( + "testing" +) + +func TestRegisterNativeTBTCSignerFallbackObserverRejectsNil(t *testing.T) { + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + err := RegisterNativeTBTCSignerFallbackObserver(nil) + if err == nil { + t.Fatal("expected registration error") + } +} + +func TestEmitNativeTBTCSignerFallbackEvent(t *testing.T) { + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + var ( + received bool + actual NativeTBTCSignerFallbackEvent + ) + + err := RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + received = true + actual = event + }, + ) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + expected := NativeTBTCSignerFallbackEvent{ + SessionID: "session-1", + Reason: "fallback reason", + KeyGroupSource: "legacy-wallet-pubkey", + LegacyPrivateKeyShareExists: true, + } + + emitNativeTBTCSignerFallbackEvent(expected) + + if !received { + t.Fatal("expected fallback event to be delivered") + } + + if actual != expected { + t.Fatalf( + "unexpected fallback event\nexpected: [%+v]\nactual: [%+v]", + expected, + actual, + ) + } +} diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index b13f831d73..01b8fdad67 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -216,6 +216,26 @@ func (n *node) setPerformanceMetrics(metrics interface { RecordDuration(name string, duration time.Duration) }) { n.performanceMetrics = metrics + + if metrics == nil { + signing.UnregisterNativeTBTCSignerFallbackObserver() + } else { + err := signing.RegisterNativeTBTCSignerFallbackObserver( + func(event signing.NativeTBTCSignerFallbackEvent) { + metrics.IncrementCounter( + clientinfo.MetricSigningNativeTBTCSignerFallbackTotal, + 1, + ) + }, + ) + if err != nil { + logger.Warnf( + "cannot register native tbtc-signer fallback observer: [%v]", + err, + ) + } + } + if n.walletDispatcher != nil { n.walletDispatcher.setMetricsRecorder(metrics) } From f2ad3ae51ca2d438a2931d8d3da61626e08fde3a Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 13:23:14 -0600 Subject: [PATCH 47/96] Wire tbtc-signer cgo bridge with runtime symbol resolution --- ...e_tbtc_signer_registration_frost_native.go | 422 +++++++++++++++++- ...c_signer_registration_frost_native_test.go | 13 +- 2 files changed, 428 insertions(+), 7 deletions(-) diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index f31164e488..b6c3c06018 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -2,9 +2,122 @@ package signing -import "fmt" +/* +#cgo CFLAGS: -std=c11 +#cgo linux LDFLAGS: -ldl +#cgo freebsd LDFLAGS: -ldl +#include +#include +#include +#include + +typedef struct { + uint8_t* ptr; + size_t len; +} TbtcBuffer; + +typedef struct { + int32_t status_code; + TbtcBuffer buffer; +} TbtcSignerResult; + +typedef TbtcSignerResult (*tbtc_start_sign_round_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef TbtcSignerResult (*tbtc_finalize_sign_round_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef void (*tbtc_free_buffer_fn)(uint8_t* ptr, size_t len); + +static TbtcSignerResult unavailable_tbtc_signer_result(void) { + TbtcSignerResult result; + result.status_code = -1; + result.buffer.ptr = NULL; + result.buffer.len = 0; + return result; +} + +static TbtcSignerResult tbtc_signer_start_sign_round(const uint8_t* request_ptr, size_t request_len) { + tbtc_start_sign_round_fn start_sign_round = (tbtc_start_sign_round_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_start_sign_round" + ); + if (start_sign_round == NULL) { + return unavailable_tbtc_signer_result(); + } + + return start_sign_round(request_ptr, request_len); +} + +static TbtcSignerResult tbtc_signer_finalize_sign_round(const uint8_t* request_ptr, size_t request_len) { + tbtc_finalize_sign_round_fn finalize_sign_round = (tbtc_finalize_sign_round_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_finalize_sign_round" + ); + if (finalize_sign_round == NULL) { + return unavailable_tbtc_signer_result(); + } + + return finalize_sign_round(request_ptr, request_len); +} + +static void tbtc_signer_free_buffer(uint8_t* ptr, size_t len) { + tbtc_free_buffer_fn free_buffer = (tbtc_free_buffer_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_free_buffer" + ); + if (free_buffer != NULL) { + free_buffer(ptr, len); + } +} +*/ +import "C" + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "unsafe" +) type buildTaggedTBTCSignerEngine struct{} +type buildTaggedTBTCSignerErrorResponse struct { + Code string `json:"code"` + Message string `json:"message"` +} + +type buildTaggedTBTCSignerStartSignRoundRequest struct { + SessionID string `json:"session_id"` + MessageHex string `json:"message_hex"` + KeyGroup string `json:"key_group"` +} + +type buildTaggedTBTCSignerStartSignRoundResponse struct { + SessionID string `json:"session_id"` + RoundID string `json:"round_id"` + RequiredContributions uint16 `json:"required_contributions"` + MessageDigestHex string `json:"message_digest_hex"` +} + +type buildTaggedTBTCSignerFinalizeSignRoundRequest struct { + SessionID string `json:"session_id"` + RoundContributions []buildTaggedTBTCSignerFinalizeRoundContribution `json:"round_contributions"` +} + +type buildTaggedTBTCSignerFinalizeRoundContribution struct { + Identifier uint16 `json:"identifier"` + SignatureShareHex string `json:"signature_share_hex"` +} + +type buildTaggedTBTCSignerFinalizeSignRoundResponse struct { + SessionID string `json:"session_id"` + RoundID string `json:"round_id"` + SignatureHex string `json:"signature_hex"` +} + +const buildTaggedTBTCSignerUnavailableStatusCode = -1 func registerBuildTaggedNativeFROSTSigningEngine() error { return RegisterNativeTBTCSignerEngine(&buildTaggedTBTCSignerEngine{}) @@ -15,19 +128,318 @@ func (bttse *buildTaggedTBTCSignerEngine) StartSignRound( message []byte, keyGroup string, ) (*NativeTBTCSignerRoundState, error) { - return nil, buildTaggedTBTCSignerBridgeNotImplementedError("StartSignRound") + requestPayload, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( + sessionID, + message, + keyGroup, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerStartSignRound(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerStartSignRoundResponse(responsePayload) } func (bttse *buildTaggedTBTCSignerEngine) FinalizeSignRound( sessionID string, roundContributions []NativeTBTCSignerRoundContribution, ) ([]byte, error) { - return nil, buildTaggedTBTCSignerBridgeNotImplementedError("FinalizeSignRound") + requestPayload, err := buildTaggedTBTCSignerFinalizeSignRoundRequestPayload( + sessionID, + roundContributions, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerFinalizeSignRound(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerFinalizeSignRoundResponse(responsePayload) } -func buildTaggedTBTCSignerBridgeNotImplementedError(operation string) error { +func buildTaggedTBTCSignerUnavailableError(operation string) error { return fmt.Errorf( - "tbtc-signer bridge operation [%v] is not implemented", + "%w: tbtc-signer bridge operation [%v] is unavailable; link libfrost_tbtc", + ErrNativeCryptographyUnavailable, operation, ) } + +func buildTaggedTBTCSignerOperationError( + operation string, + message string, +) error { + return fmt.Errorf( + "%w: tbtc-signer bridge operation [%v] failed: [%s]", + ErrNativeCryptographyUnavailable, + operation, + message, + ) +} + +func buildTaggedTBTCSignerStartSignRoundRequestPayload( + sessionID string, + message []byte, + keyGroup string, +) ([]byte, error) { + if sessionID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "session ID is empty", + ) + } + + if keyGroup == "" { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "key group is empty", + ) + } + + request := buildTaggedTBTCSignerStartSignRoundRequest{ + SessionID: sessionID, + MessageHex: hex.EncodeToString(message), + KeyGroup: keyGroup, + } + + payload, err := json.Marshal(request) + if err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + fmt.Sprintf("cannot marshal request: %v", err), + ) + } + + return payload, nil +} + +func decodeBuildTaggedTBTCSignerStartSignRoundResponse( + responsePayload []byte, +) (*NativeTBTCSignerRoundState, error) { + var response buildTaggedTBTCSignerStartSignRoundResponse + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + if response.SessionID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "response session ID is empty", + ) + } + + if response.RoundID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "response round ID is empty", + ) + } + + if response.MessageDigestHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "response message digest is empty", + ) + } + + return &NativeTBTCSignerRoundState{ + SessionID: response.SessionID, + RoundID: response.RoundID, + RequiredContributions: response.RequiredContributions, + MessageDigestHex: response.MessageDigestHex, + }, nil +} + +func buildTaggedTBTCSignerFinalizeSignRoundRequestPayload( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, +) ([]byte, error) { + if sessionID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "FinalizeSignRound", + "session ID is empty", + ) + } + + if len(roundContributions) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "FinalizeSignRound", + "round contributions are empty", + ) + } + + payloadContributions := make( + []buildTaggedTBTCSignerFinalizeRoundContribution, + 0, + len(roundContributions), + ) + + for i, contribution := range roundContributions { + if len(contribution.Data) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "FinalizeSignRound", + fmt.Sprintf("round contribution [%d] data is empty", i), + ) + } + + payloadContributions = append( + payloadContributions, + buildTaggedTBTCSignerFinalizeRoundContribution{ + Identifier: contribution.Identifier, + SignatureShareHex: hex.EncodeToString(contribution.Data), + }, + ) + } + + request := buildTaggedTBTCSignerFinalizeSignRoundRequest{ + SessionID: sessionID, + RoundContributions: payloadContributions, + } + + payload, err := json.Marshal(request) + if err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "FinalizeSignRound", + fmt.Sprintf("cannot marshal request: %v", err), + ) + } + + return payload, nil +} + +func decodeBuildTaggedTBTCSignerFinalizeSignRoundResponse( + responsePayload []byte, +) ([]byte, error) { + var response buildTaggedTBTCSignerFinalizeSignRoundResponse + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "FinalizeSignRound", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + if response.SignatureHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "FinalizeSignRound", + "response signature is empty", + ) + } + + signature, err := hex.DecodeString(response.SignatureHex) + if err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "FinalizeSignRound", + fmt.Sprintf("response signature is invalid hex: %v", err), + ) + } + + return signature, nil +} + +func callBuildTaggedTBTCSignerStartSignRound( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "StartSignRound", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_start_sign_round(requestPtr, requestLen) + }, + ) +} + +func callBuildTaggedTBTCSignerFinalizeSignRound( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "FinalizeSignRound", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_finalize_sign_round(requestPtr, requestLen) + }, + ) +} + +func callBuildTaggedTBTCSignerOperation( + operation string, + requestPayload []byte, + call func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult, +) ([]byte, error) { + if len(requestPayload) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + "request payload is empty", + ) + } + + requestPtr := C.CBytes(requestPayload) + defer C.free(requestPtr) + + result := call((*C.uint8_t)(requestPtr), C.size_t(len(requestPayload))) + return parseBuildTaggedTBTCSignerResult(operation, result) +} + +func parseBuildTaggedTBTCSignerResult( + operation string, + result C.TbtcSignerResult, +) ([]byte, error) { + defer C.tbtc_signer_free_buffer(result.buffer.ptr, result.buffer.len) + + statusCode := int32(result.status_code) + if statusCode == buildTaggedTBTCSignerUnavailableStatusCode { + return nil, buildTaggedTBTCSignerUnavailableError(operation) + } + + var payload []byte + if result.buffer.ptr != nil && result.buffer.len > 0 { + payload = C.GoBytes(unsafe.Pointer(result.buffer.ptr), C.int(result.buffer.len)) + } + + if statusCode != 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + buildTaggedTBTCSignerErrorMessage(payload), + ) + } + + if len(payload) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + "response payload is empty", + ) + } + + return payload, nil +} + +func buildTaggedTBTCSignerErrorMessage(payload []byte) string { + var errorResponse buildTaggedTBTCSignerErrorResponse + if err := json.Unmarshal(payload, &errorResponse); err != nil { + return fmt.Sprintf( + "cannot decode error payload [%x]: %v", + payload, + err, + ) + } + + if errorResponse.Code == "" && errorResponse.Message == "" { + return fmt.Sprintf("empty error payload: [%s]", string(payload)) + } + + if errorResponse.Code != "" { + return fmt.Sprintf("%s: %s", errorResponse.Code, errorResponse.Message) + } + + return errorResponse.Message +} diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 7731adae64..904355451c 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -3,6 +3,7 @@ package signing import ( + "errors" "strings" "testing" ) @@ -29,10 +30,18 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { "key-group", ) if err == nil { - t.Fatal("expected not-implemented tbtc-signer bridge error") + t.Fatal("expected unavailable tbtc-signer bridge error") } - if !strings.Contains(err.Error(), "not implemented") { + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected native cryptography unavailable error: [%v], got [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if !strings.Contains(err.Error(), "unavailable") { t.Fatalf("unexpected bridge error: [%v]", err) } } From b82db0524194a7aa271bc9a900c7766627afa6bb Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 13:25:51 -0600 Subject: [PATCH 48/96] Add bridge payload/response tests for tbtc-signer cgo engine --- ...c_signer_registration_frost_native_test.go | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 904355451c..8fd371f332 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -3,6 +3,7 @@ package signing import ( + "encoding/json" "errors" "strings" "testing" @@ -45,3 +46,182 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { t.Fatalf("unexpected bridge error: [%v]", err) } } + +func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload(t *testing.T) { + payload, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( + "session-1", + []byte{0xab, 0xcd}, + "key-group-1", + ) + if err != nil { + t.Fatalf("unexpected payload build error: [%v]", err) + } + + var request buildTaggedTBTCSignerStartSignRoundRequest + if err := json.Unmarshal(payload, &request); err != nil { + t.Fatalf("cannot decode request payload: [%v]", err) + } + + if request.SessionID != "session-1" { + t.Fatalf( + "unexpected session id\nexpected: [%v]\nactual: [%v]", + "session-1", + request.SessionID, + ) + } + + if request.MessageHex != "abcd" { + t.Fatalf( + "unexpected message hex\nexpected: [%v]\nactual: [%v]", + "abcd", + request.MessageHex, + ) + } + + if request.KeyGroup != "key-group-1" { + t.Fatalf( + "unexpected key group\nexpected: [%v]\nactual: [%v]", + "key-group-1", + request.KeyGroup, + ) + } +} + +func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_EmptySessionID(t *testing.T) { + _, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( + "", + []byte{0xab}, + "key-group-1", + ) + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected native cryptography unavailable error: [%v], got [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } +} + +func TestBuildTaggedTBTCSignerFinalizeSignRoundRequestPayload(t *testing.T) { + payload, err := buildTaggedTBTCSignerFinalizeSignRoundRequestPayload( + "session-1", + []NativeTBTCSignerRoundContribution{ + { + Identifier: 7, + Data: []byte{0xde, 0xad}, + }, + }, + ) + if err != nil { + t.Fatalf("unexpected payload build error: [%v]", err) + } + + var request buildTaggedTBTCSignerFinalizeSignRoundRequest + if err := json.Unmarshal(payload, &request); err != nil { + t.Fatalf("cannot decode request payload: [%v]", err) + } + + if request.SessionID != "session-1" { + t.Fatalf( + "unexpected session id\nexpected: [%v]\nactual: [%v]", + "session-1", + request.SessionID, + ) + } + + if len(request.RoundContributions) != 1 { + t.Fatalf( + "unexpected contribution count\nexpected: [%v]\nactual: [%v]", + 1, + len(request.RoundContributions), + ) + } + + if request.RoundContributions[0].Identifier != 7 { + t.Fatalf( + "unexpected contribution identifier\nexpected: [%v]\nactual: [%v]", + 7, + request.RoundContributions[0].Identifier, + ) + } + + if request.RoundContributions[0].SignatureShareHex != "dead" { + t.Fatalf( + "unexpected contribution signature share\nexpected: [%v]\nactual: [%v]", + "dead", + request.RoundContributions[0].SignatureShareHex, + ) + } +} + +func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse(t *testing.T) { + roundState, err := decodeBuildTaggedTBTCSignerStartSignRoundResponse( + []byte( + `{"session_id":"session-1","round_id":"round-1","required_contributions":2,"message_digest_hex":"abcd"}`, + ), + ) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + if roundState.SessionID != "session-1" { + t.Fatalf( + "unexpected session id\nexpected: [%v]\nactual: [%v]", + "session-1", + roundState.SessionID, + ) + } + + if roundState.RoundID != "round-1" { + t.Fatalf( + "unexpected round id\nexpected: [%v]\nactual: [%v]", + "round-1", + roundState.RoundID, + ) + } + + if roundState.RequiredContributions != 2 { + t.Fatalf( + "unexpected required contributions\nexpected: [%v]\nactual: [%v]", + 2, + roundState.RequiredContributions, + ) + } + + if roundState.MessageDigestHex != "abcd" { + t.Fatalf( + "unexpected message digest hex\nexpected: [%v]\nactual: [%v]", + "abcd", + roundState.MessageDigestHex, + ) + } +} + +func TestDecodeBuildTaggedTBTCSignerFinalizeSignRoundResponse(t *testing.T) { + signature, err := decodeBuildTaggedTBTCSignerFinalizeSignRoundResponse( + []byte(`{"session_id":"session-1","round_id":"round-1","signature_hex":"deadbeef"}`), + ) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + expectedSignature := []byte{0xde, 0xad, 0xbe, 0xef} + if len(signature) != len(expectedSignature) { + t.Fatalf( + "unexpected signature length\nexpected: [%v]\nactual: [%v]", + len(expectedSignature), + len(signature), + ) + } + + for i := range signature { + if signature[i] != expectedSignature[i] { + t.Fatalf( + "unexpected signature byte at index [%d]\nexpected: [%x]\nactual: [%x]", + i, + expectedSignature[i], + signature[i], + ) + } + } +} From 4ff54051e331ed7f201be97e858dc5bc8adaec21 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 13:33:30 -0600 Subject: [PATCH 49/96] Add RunDKG support to tbtc-signer coarse bridge --- ...rimitive_transitional_frost_native_test.go | 8 + ...e_tbtc_signer_registration_frost_native.go | 190 ++++++++++++++++++ ...c_signer_registration_frost_native_test.go | 183 +++++++++++++++++ .../native_tbtc_signer_engine_frost_native.go | 21 ++ ...ve_tbtc_signer_engine_frost_native_test.go | 8 + 5 files changed, 410 insertions(+) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 72c5e8115e..3b16b25337 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -21,6 +21,14 @@ type mockBuildTaggedTBTCSignerEngine struct { keyGroup string } +func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, +) (*NativeTBTCSignerDKGResult, error) { + return nil, fmt.Errorf("not used") +} + func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( sessionID string, message []byte, diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index b6c3c06018..a9baf881ff 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -21,6 +21,10 @@ typedef struct { TbtcBuffer buffer; } TbtcSignerResult; +typedef TbtcSignerResult (*tbtc_run_dkg_fn)( + const uint8_t* request_ptr, + size_t request_len +); typedef TbtcSignerResult (*tbtc_start_sign_round_fn)( const uint8_t* request_ptr, size_t request_len @@ -39,6 +43,18 @@ static TbtcSignerResult unavailable_tbtc_signer_result(void) { return result; } +static TbtcSignerResult tbtc_signer_run_dkg(const uint8_t* request_ptr, size_t request_len) { + tbtc_run_dkg_fn run_dkg = (tbtc_run_dkg_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_run_dkg" + ); + if (run_dkg == NULL) { + return unavailable_tbtc_signer_result(); + } + + return run_dkg(request_ptr, request_len); +} + static TbtcSignerResult tbtc_signer_start_sign_round(const uint8_t* request_ptr, size_t request_len) { tbtc_start_sign_round_fn start_sign_round = (tbtc_start_sign_round_fn)dlsym( RTLD_DEFAULT, @@ -88,6 +104,25 @@ type buildTaggedTBTCSignerErrorResponse struct { Message string `json:"message"` } +type buildTaggedTBTCSignerRunDKGRequest struct { + SessionID string `json:"session_id"` + Participants []buildTaggedTBTCSignerDKGParticipant `json:"participants"` + Threshold uint16 `json:"threshold"` +} + +type buildTaggedTBTCSignerDKGParticipant struct { + Identifier uint16 `json:"identifier"` + PublicKeyHex string `json:"public_key_hex"` +} + +type buildTaggedTBTCSignerRunDKGResponse struct { + SessionID string `json:"session_id"` + KeyGroup string `json:"key_group"` + ParticipantCount uint16 `json:"participant_count"` + Threshold uint16 `json:"threshold"` + CreatedAtUnix uint64 `json:"created_at_unix"` +} + type buildTaggedTBTCSignerStartSignRoundRequest struct { SessionID string `json:"session_id"` MessageHex string `json:"message_hex"` @@ -123,6 +158,28 @@ func registerBuildTaggedNativeFROSTSigningEngine() error { return RegisterNativeTBTCSignerEngine(&buildTaggedTBTCSignerEngine{}) } +func (bttse *buildTaggedTBTCSignerEngine) RunDKG( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, +) (*NativeTBTCSignerDKGResult, error) { + requestPayload, err := buildTaggedTBTCSignerRunDKGRequestPayload( + sessionID, + participants, + threshold, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerRunDKG(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerRunDKGResponse(responsePayload) +} + func (bttse *buildTaggedTBTCSignerEngine) StartSignRound( sessionID string, message []byte, @@ -185,6 +242,127 @@ func buildTaggedTBTCSignerOperationError( ) } +func buildTaggedTBTCSignerRunDKGRequestPayload( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, +) ([]byte, error) { + if sessionID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + "session ID is empty", + ) + } + + if len(participants) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + "participants are empty", + ) + } + + if threshold == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + "threshold is zero", + ) + } + + requestParticipants := make( + []buildTaggedTBTCSignerDKGParticipant, + 0, + len(participants), + ) + + for i, participant := range participants { + if participant.Identifier == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + fmt.Sprintf("participant [%d] identifier is zero", i), + ) + } + + if participant.PublicKeyHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + fmt.Sprintf("participant [%d] public key hex is empty", i), + ) + } + + requestParticipants = append( + requestParticipants, + buildTaggedTBTCSignerDKGParticipant{ + Identifier: participant.Identifier, + PublicKeyHex: participant.PublicKeyHex, + }, + ) + } + + request := buildTaggedTBTCSignerRunDKGRequest{ + SessionID: sessionID, + Participants: requestParticipants, + Threshold: threshold, + } + + payload, err := json.Marshal(request) + if err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + fmt.Sprintf("cannot marshal request: %v", err), + ) + } + + return payload, nil +} + +func decodeBuildTaggedTBTCSignerRunDKGResponse( + responsePayload []byte, +) (*NativeTBTCSignerDKGResult, error) { + var response buildTaggedTBTCSignerRunDKGResponse + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + if response.SessionID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + "response session ID is empty", + ) + } + + if response.KeyGroup == "" { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + "response key group is empty", + ) + } + + if response.ParticipantCount == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + "response participant count is zero", + ) + } + + if response.Threshold == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + "response threshold is zero", + ) + } + + return &NativeTBTCSignerDKGResult{ + SessionID: response.SessionID, + KeyGroup: response.KeyGroup, + ParticipantCount: response.ParticipantCount, + Threshold: response.Threshold, + CreatedAtUnix: response.CreatedAtUnix, + }, nil +} + func buildTaggedTBTCSignerStartSignRoundRequestPayload( sessionID string, message []byte, @@ -347,6 +525,18 @@ func decodeBuildTaggedTBTCSignerFinalizeSignRoundResponse( return signature, nil } +func callBuildTaggedTBTCSignerRunDKG( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "RunDKG", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_run_dkg(requestPtr, requestLen) + }, + ) +} + func callBuildTaggedTBTCSignerStartSignRound( requestPayload []byte, ) ([]byte, error) { diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 8fd371f332..07e3106b2a 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -47,6 +47,189 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { } } +func TestBuildTaggedTBTCSignerRunDKGRequestPayload(t *testing.T) { + payload, err := buildTaggedTBTCSignerRunDKGRequestPayload( + "session-1", + []NativeTBTCSignerDKGParticipant{ + { + Identifier: 1, + PublicKeyHex: "02aa", + }, + { + Identifier: 2, + PublicKeyHex: "02bb", + }, + }, + 2, + ) + if err != nil { + t.Fatalf("unexpected payload build error: [%v]", err) + } + + var request buildTaggedTBTCSignerRunDKGRequest + if err := json.Unmarshal(payload, &request); err != nil { + t.Fatalf("cannot decode request payload: [%v]", err) + } + + if request.SessionID != "session-1" { + t.Fatalf( + "unexpected session id\nexpected: [%v]\nactual: [%v]", + "session-1", + request.SessionID, + ) + } + + if request.Threshold != 2 { + t.Fatalf( + "unexpected threshold\nexpected: [%v]\nactual: [%v]", + 2, + request.Threshold, + ) + } + + if len(request.Participants) != 2 { + t.Fatalf( + "unexpected participants count\nexpected: [%v]\nactual: [%v]", + 2, + len(request.Participants), + ) + } + + if request.Participants[0].Identifier != 1 { + t.Fatalf( + "unexpected participant identifier\nexpected: [%v]\nactual: [%v]", + 1, + request.Participants[0].Identifier, + ) + } + + if request.Participants[0].PublicKeyHex != "02aa" { + t.Fatalf( + "unexpected participant public key hex\nexpected: [%v]\nactual: [%v]", + "02aa", + request.Participants[0].PublicKeyHex, + ) + } +} + +func TestBuildTaggedTBTCSignerRunDKGRequestPayload_RejectsInvalidInput(t *testing.T) { + testCases := []struct { + name string + sessionID string + participants []NativeTBTCSignerDKGParticipant + threshold uint16 + }{ + { + name: "empty session id", + sessionID: "", + participants: []NativeTBTCSignerDKGParticipant{{Identifier: 1, PublicKeyHex: "02aa"}}, + threshold: 2, + }, + { + name: "empty participants", + sessionID: "session-1", + participants: nil, + threshold: 2, + }, + { + name: "zero threshold", + sessionID: "session-1", + participants: []NativeTBTCSignerDKGParticipant{ + {Identifier: 1, PublicKeyHex: "02aa"}, + }, + threshold: 0, + }, + { + name: "participant zero identifier", + sessionID: "session-1", + participants: []NativeTBTCSignerDKGParticipant{ + {Identifier: 0, PublicKeyHex: "02aa"}, + }, + threshold: 1, + }, + { + name: "participant empty public key hex", + sessionID: "session-1", + participants: []NativeTBTCSignerDKGParticipant{ + {Identifier: 1, PublicKeyHex: ""}, + }, + threshold: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := buildTaggedTBTCSignerRunDKGRequestPayload( + tc.sessionID, + tc.participants, + tc.threshold, + ) + if err == nil { + t.Fatal("expected payload build error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected native cryptography unavailable error: [%v], got [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + }) + } +} + +func TestDecodeBuildTaggedTBTCSignerRunDKGResponse(t *testing.T) { + result, err := decodeBuildTaggedTBTCSignerRunDKGResponse( + []byte( + `{"session_id":"session-1","key_group":"group-1","participant_count":3,"threshold":2,"created_at_unix":123456789}`, + ), + ) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + if result.SessionID != "session-1" { + t.Fatalf( + "unexpected session id\nexpected: [%v]\nactual: [%v]", + "session-1", + result.SessionID, + ) + } + + if result.KeyGroup != "group-1" { + t.Fatalf( + "unexpected key group\nexpected: [%v]\nactual: [%v]", + "group-1", + result.KeyGroup, + ) + } + + if result.ParticipantCount != 3 { + t.Fatalf( + "unexpected participant count\nexpected: [%v]\nactual: [%v]", + 3, + result.ParticipantCount, + ) + } + + if result.Threshold != 2 { + t.Fatalf( + "unexpected threshold\nexpected: [%v]\nactual: [%v]", + 2, + result.Threshold, + ) + } + + if result.CreatedAtUnix != 123456789 { + t.Fatalf( + "unexpected created-at unix\nexpected: [%v]\nactual: [%v]", + 123456789, + result.CreatedAtUnix, + ) + } +} + func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload(t *testing.T) { payload, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( "session-1", diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go index 39c5dc3bd0..b703d506f5 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -18,6 +18,22 @@ type NativeTBTCSignerMaterialPayload struct { LegacyPrivateKeyShareHex string `json:"legacyPrivateKeyShareHex,omitempty"` } +// NativeTBTCSignerDKGParticipant identifies a DKG participant for coarse +// tbtc-signer RunDKG operation. +type NativeTBTCSignerDKGParticipant struct { + Identifier uint16 `json:"identifier"` + PublicKeyHex string `json:"publicKeyHex"` +} + +// NativeTBTCSignerDKGResult captures DKG result metadata returned by RunDKG. +type NativeTBTCSignerDKGResult struct { + SessionID string `json:"sessionID"` + KeyGroup string `json:"keyGroup"` + ParticipantCount uint16 `json:"participantCount"` + Threshold uint16 `json:"threshold"` + CreatedAtUnix uint64 `json:"createdAtUnix"` +} + // NativeTBTCSignerRoundContribution is a participant contribution consumed by // tbtc-signer during signature finalization. type NativeTBTCSignerRoundContribution struct { @@ -37,6 +53,11 @@ type NativeTBTCSignerRoundState struct { // NativeTBTCSignerEngine executes coarse, session-keyed tbtc-signer // operations. type NativeTBTCSignerEngine interface { + RunDKG( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + ) (*NativeTBTCSignerDKGResult, error) StartSignRound( sessionID string, message []byte, diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go index 4cf55734ff..088cf62f9b 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go @@ -9,6 +9,14 @@ import ( type mockNativeTBTCSignerEngine struct{} +func (mntse *mockNativeTBTCSignerEngine) RunDKG( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, +) (*NativeTBTCSignerDKGResult, error) { + return nil, fmt.Errorf("not implemented") +} + func (mntse *mockNativeTBTCSignerEngine) StartSignRound( sessionID string, message []byte, From 29a46ab96ce7ddb09adb67ceca65c90e21b25dea Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 13:40:01 -0600 Subject: [PATCH 50/96] Invoke RunDKG in transitional tbtc-signer signing path --- ...ffi_primitive_transitional_frost_native.go | 136 +++++++++++++- ...rimitive_transitional_frost_native_test.go | 166 ++++++++++++++++-- 2 files changed, 282 insertions(+), 20 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 54c58fb589..1fc292bbe9 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -102,11 +102,82 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) return nil, err } - // Do not start coarse native sessions until finalize flow is wired. Calling - // StartSignRound without finalize would orphan signer-engine state. - if currentNativeTBTCSignerEngine() != nil && logger != nil { - logger.Warnf( - "native tbtc-signer engine is registered but coarse finalize flow is not wired; using legacy fallback", + nativeEngine := currentNativeTBTCSignerEngine() + if nativeEngine == nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "native tbtc-signer engine is unavailable", + payload.KeyGroupSource, + ) + } + + dkgParticipants, dkgThreshold, err := buildTaggedTBTCSignerRunDKGInputs(request) + if err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf("cannot prepare tbtc-signer RunDKG request: [%v]", err), + payload.KeyGroupSource, + ) + } + + dkgResult, err := nativeEngine.RunDKG( + request.SessionID, + dkgParticipants, + dkgThreshold, + ) + if err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "tbtc-signer RunDKG failed", + payload.KeyGroupSource, + ) + } + + if dkgResult == nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "tbtc-signer RunDKG returned nil result", + payload.KeyGroupSource, + ) + } + + if dkgResult.KeyGroup == "" { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "tbtc-signer RunDKG returned empty key group", + payload.KeyGroupSource, + ) + } + + if payload.KeyGroup != dkgResult.KeyGroup { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "tbtc-signer key group does not match RunDKG result", + payload.KeyGroupSource, + ) + } + + if logger != nil { + logger.Debugf( + "validated tbtc-signer key-group contract via RunDKG; using legacy fallback until finalize flow is wired", ) } @@ -118,14 +189,61 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) logger, request, legacyPrivateKeyShare, - fmt.Sprintf( - "tbtc-signer coarse session flow is not wired (keyGroupSource=%s)", - payload.KeyGroupSource, - ), + "tbtc-signer RunDKG is wired but coarse finalize flow is not wired", payload.KeyGroupSource, ) } +func buildTaggedTBTCSignerRunDKGInputs( + request *NativeExecutionFFISigningRequest, +) ([]NativeTBTCSignerDKGParticipant, uint16, error) { + _, includedMembersIndexes, err := includedMembersFromRequest(request) + if err != nil { + return nil, 0, err + } + + if len(includedMembersIndexes) < 2 { + return nil, 0, fmt.Errorf("insufficient included members for DKG") + } + + threshold := request.DishonestThreshold + 1 + if threshold < 2 { + return nil, 0, fmt.Errorf("derived threshold is below minimum: [%v]", threshold) + } + + if threshold > len(includedMembersIndexes) { + return nil, 0, fmt.Errorf( + "derived threshold exceeds included members count: [%v] > [%v]", + threshold, + len(includedMembersIndexes), + ) + } + + participants := make([]NativeTBTCSignerDKGParticipant, 0, len(includedMembersIndexes)) + for _, memberIndex := range includedMembersIndexes { + if memberIndex == 0 { + return nil, 0, fmt.Errorf("included member index is zero") + } + + identifier := uint16(memberIndex) + participants = append( + participants, + NativeTBTCSignerDKGParticipant{ + Identifier: identifier, + PublicKeyHex: buildTaggedTBTCSignerDKGPlaceholderPublicKeyHex(identifier), + }, + ) + } + + return participants, uint16(threshold), nil +} + +func buildTaggedTBTCSignerDKGPlaceholderPublicKeyHex(identifier uint16) string { + // Transitional placeholder until canonical member public keys are available + // in the native signing request path. + return fmt.Sprintf("02%04x", identifier) +} + func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithLegacyTECDSABridge( ctx context.Context, logger log.StandardLogger, diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 3b16b25337..82b41d1dc3 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -11,14 +11,21 @@ import ( "testing" "github.com/keep-network/keep-core/pkg/internal/tecdsatest" + "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" ) type mockBuildTaggedTBTCSignerEngine struct { - startCalled bool - sessionID string - message []byte - keyGroup string + runDKGCalled bool + runDKGSessionID string + runDKGParticipants []NativeTBTCSignerDKGParticipant + runDKGThreshold uint16 + runDKGResult *NativeTBTCSignerDKGResult + runDKGErr error + startCalled bool + startSessionID string + startMessage []byte + startKeyGroup string } func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( @@ -26,7 +33,29 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( participants []NativeTBTCSignerDKGParticipant, threshold uint16, ) (*NativeTBTCSignerDKGResult, error) { - return nil, fmt.Errorf("not used") + mbttse.runDKGCalled = true + mbttse.runDKGSessionID = sessionID + mbttse.runDKGParticipants = append( + []NativeTBTCSignerDKGParticipant{}, + participants..., + ) + mbttse.runDKGThreshold = threshold + + if mbttse.runDKGErr != nil { + return nil, mbttse.runDKGErr + } + + if mbttse.runDKGResult != nil { + return mbttse.runDKGResult, nil + } + + return &NativeTBTCSignerDKGResult{ + SessionID: sessionID, + KeyGroup: "group-1", + ParticipantCount: uint16(len(participants)), + Threshold: threshold, + CreatedAtUnix: 1, + }, nil } func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( @@ -35,9 +64,9 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( keyGroup string, ) (*NativeTBTCSignerRoundState, error) { mbttse.startCalled = true - mbttse.sessionID = sessionID - mbttse.message = append([]byte{}, message...) - mbttse.keyGroup = keyGroup + mbttse.startSessionID = sessionID + mbttse.startMessage = append([]byte{}, message...) + mbttse.startKeyGroup = keyGroup return &NativeTBTCSignerRoundState{ SessionID: sessionID, @@ -365,6 +394,91 @@ func TestDecodeBuildTaggedTBTCSignerLegacyPrivateKeyShare_RejectsInvalidPayload( } } +func TestBuildTaggedTBTCSignerRunDKGInputs(t *testing.T) { + participants, threshold, err := buildTaggedTBTCSignerRunDKGInputs( + &NativeExecutionFFISigningRequest{ + GroupSize: 5, + DishonestThreshold: 2, + Attempt: &Attempt{ + IncludedMembersIndexes: []group.MemberIndex{1, 3, 5}, + }, + }, + ) + if err != nil { + t.Fatalf("unexpected RunDKG inputs error: [%v]", err) + } + + if threshold != 3 { + t.Fatalf( + "unexpected threshold\nexpected: [%v]\nactual: [%v]", + 3, + threshold, + ) + } + + if len(participants) != 3 { + t.Fatalf( + "unexpected participants count\nexpected: [%v]\nactual: [%v]", + 3, + len(participants), + ) + } + + expectedIdentifiers := []uint16{1, 3, 5} + expectedPublicKeys := []string{"020001", "020003", "020005"} + + for i := range participants { + if participants[i].Identifier != expectedIdentifiers[i] { + t.Fatalf( + "unexpected participant identifier at index [%d]\nexpected: [%v]\nactual: [%v]", + i, + expectedIdentifiers[i], + participants[i].Identifier, + ) + } + + if participants[i].PublicKeyHex != expectedPublicKeys[i] { + t.Fatalf( + "unexpected participant public key at index [%d]\nexpected: [%v]\nactual: [%v]", + i, + expectedPublicKeys[i], + participants[i].PublicKeyHex, + ) + } + } +} + +func TestBuildTaggedTBTCSignerRunDKGInputs_RejectsInvalidRequest(t *testing.T) { + testCases := []struct { + name string + request *NativeExecutionFFISigningRequest + }{ + { + name: "zero group size", + request: &NativeExecutionFFISigningRequest{ + GroupSize: 0, + DishonestThreshold: 1, + }, + }, + { + name: "derived threshold exceeds participants", + request: &NativeExecutionFFISigningRequest{ + GroupSize: 2, + DishonestThreshold: 2, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, _, err := buildTaggedTBTCSignerRunDKGInputs(tc.request) + if err == nil { + t.Fatal("expected error") + } + }) + } +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath( t *testing.T, ) { @@ -380,8 +494,11 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ - Message: big.NewInt(123), - SessionID: "session-1", + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, SignerMaterial: &NativeSignerMaterial{ Format: NativeSignerMaterialFormatFrostTBTCSignerV1, Payload: []byte(`{"keyGroup":"group-1"}`), @@ -399,10 +516,37 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } + if !engine.runDKGCalled { + t.Fatal("expected RunDKG call in tbtc-signer path") + } + + if engine.runDKGSessionID != "session-1" { + t.Fatalf( + "unexpected RunDKG session ID\nexpected: [%v]\nactual: [%v]", + "session-1", + engine.runDKGSessionID, + ) + } + + if engine.runDKGThreshold != 2 { + t.Fatalf( + "unexpected RunDKG threshold\nexpected: [%v]\nactual: [%v]", + 2, + engine.runDKGThreshold, + ) + } + + if len(engine.runDKGParticipants) != 3 { + t.Fatalf( + "unexpected RunDKG participants count\nexpected: [%v]\nactual: [%v]", + 3, + len(engine.runDKGParticipants), + ) + } + if engine.startCalled { t.Fatal("did not expect StartSignRound call while coarse finalize flow is unwired") } - } func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_NoEngineNoLegacyShare( From af5fe8764cb2e2e2ea9922751f5349187c69b383 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 13:46:28 -0600 Subject: [PATCH 51/96] Gate coarse round scaffold on bootstrap tbtc-signer version --- ...ffi_primitive_transitional_frost_native.go | 171 +++++++++++++++++- ...rimitive_transitional_frost_native_test.go | 150 ++++++++++++++- ...e_tbtc_signer_registration_frost_native.go | 35 ++++ ...c_signer_registration_frost_native_test.go | 20 ++ 4 files changed, 368 insertions(+), 8 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 1fc292bbe9..8396bc6393 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "strings" "github.com/ipfs/go-log/v2" "github.com/keep-network/keep-core/pkg/frost" @@ -35,6 +36,12 @@ func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( // is wired end-to-end. type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} +const buildTaggedTBTCSignerBootstrapVersionToken = "bootstrap" + +type nativeTBTCSignerVersionedEngine interface { + Version() (string, error) +} + func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) Sign( ctx context.Context, logger log.StandardLogger, @@ -175,21 +182,74 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) ) } + versionedEngine, isVersioned := nativeEngine.(nativeTBTCSignerVersionedEngine) + if !isVersioned { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "tbtc-signer version API is unavailable; coarse round scaffold skipped", + payload.KeyGroupSource, + ) + } + + engineVersion, err := versionedEngine.Version() + if err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "cannot query tbtc-signer version; coarse round scaffold skipped", + payload.KeyGroupSource, + ) + } + + if !strings.Contains( + strings.ToLower(engineVersion), + buildTaggedTBTCSignerBootstrapVersionToken, + ) { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf( + "tbtc-signer version [%s] is not bootstrap; coarse round scaffold skipped", + engineVersion, + ), + payload.KeyGroupSource, + ) + } + + if err := executeBuildTaggedTBTCSignerBootstrapCoarseRound( + request, + payload.KeyGroup, + nativeEngine, + ); err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "tbtc-signer bootstrap coarse round failed", + payload.KeyGroupSource, + ) + } + if logger != nil { logger.Debugf( - "validated tbtc-signer key-group contract via RunDKG; using legacy fallback until finalize flow is wired", + "validated tbtc-signer key-group contract via RunDKG and bootstrap coarse round; using legacy fallback until signature cutover", ) } - // The coarse-session flow is intentionally deferred until keep-core - // orchestration is migrated from round-level message exchange. Use a Go-side - // legacy fallback while this migration is in progress. return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, logger, request, legacyPrivateKeyShare, - "tbtc-signer RunDKG is wired but coarse finalize flow is not wired", + "tbtc-signer bootstrap coarse round completed; using legacy fallback during migration", payload.KeyGroupSource, ) } @@ -244,6 +304,107 @@ func buildTaggedTBTCSignerDKGPlaceholderPublicKeyHex(identifier uint16) string { return fmt.Sprintf("02%04x", identifier) } +func executeBuildTaggedTBTCSignerBootstrapCoarseRound( + request *NativeExecutionFFISigningRequest, + keyGroup string, + nativeEngine NativeTBTCSignerEngine, +) error { + if request == nil { + return fmt.Errorf("request is nil") + } + + if request.Message == nil { + return fmt.Errorf("request message is nil") + } + + if nativeEngine == nil { + return fmt.Errorf("native tbtc-signer engine is nil") + } + + messageBytes := request.Message.Bytes() + if len(messageBytes) == 0 { + messageBytes = []byte{0} + } + + roundState, err := nativeEngine.StartSignRound( + request.SessionID, + messageBytes, + keyGroup, + ) + if err != nil { + return fmt.Errorf("start sign round failed: [%w]", err) + } + + if roundState == nil { + return fmt.Errorf("start sign round returned nil state") + } + + if roundState.RequiredContributions == 0 { + return fmt.Errorf("start sign round required contributions are zero") + } + + _, includedMembersIndexes, err := includedMembersFromRequest(request) + if err != nil { + return fmt.Errorf("cannot determine included members: [%w]", err) + } + + roundContributions := buildTaggedTBTCSignerSyntheticRoundContributions( + includedMembersIndexes, + ) + if len(roundContributions) < int(roundState.RequiredContributions) { + return fmt.Errorf( + "insufficient synthetic round contributions: [%v] < [%v]", + len(roundContributions), + roundState.RequiredContributions, + ) + } + + signature, err := nativeEngine.FinalizeSignRound( + request.SessionID, + roundContributions, + ) + if err != nil { + return fmt.Errorf("finalize sign round failed: [%w]", err) + } + + if len(signature) == 0 { + return fmt.Errorf("finalize sign round returned empty signature") + } + + return nil +} + +func buildTaggedTBTCSignerSyntheticRoundContributions( + includedMembersIndexes []group.MemberIndex, +) []NativeTBTCSignerRoundContribution { + contributions := make( + []NativeTBTCSignerRoundContribution, + 0, + len(includedMembersIndexes), + ) + + for _, memberIndex := range includedMembersIndexes { + if memberIndex == 0 { + continue + } + + identifier := uint16(memberIndex) + contributions = append( + contributions, + NativeTBTCSignerRoundContribution{ + Identifier: identifier, + Data: []byte{ + byte(identifier >> 8), + byte(identifier), + 0x01, + }, + }, + ) + } + + return contributions +} + func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithLegacyTECDSABridge( ctx context.Context, logger log.StandardLogger, diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 82b41d1dc3..420c886146 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -6,7 +6,6 @@ import ( "bytes" "encoding/hex" "errors" - "fmt" "math/big" "testing" @@ -22,10 +21,19 @@ type mockBuildTaggedTBTCSignerEngine struct { runDKGThreshold uint16 runDKGResult *NativeTBTCSignerDKGResult runDKGErr error + version string + versionErr error startCalled bool startSessionID string startMessage []byte startKeyGroup string + startRoundState *NativeTBTCSignerRoundState + startErr error + finalizeCalled bool + finalizeSessionID string + finalizeInputs []NativeTBTCSignerRoundContribution + finalizeSignature []byte + finalizeErr error } func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( @@ -58,6 +66,14 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( }, nil } +func (mbttse *mockBuildTaggedTBTCSignerEngine) Version() (string, error) { + if mbttse.versionErr != nil { + return "", mbttse.versionErr + } + + return mbttse.version, nil +} + func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( sessionID string, message []byte, @@ -68,6 +84,14 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( mbttse.startMessage = append([]byte{}, message...) mbttse.startKeyGroup = keyGroup + if mbttse.startErr != nil { + return nil, mbttse.startErr + } + + if mbttse.startRoundState != nil { + return mbttse.startRoundState, nil + } + return &NativeTBTCSignerRoundState{ SessionID: sessionID, RoundID: "round-1", @@ -80,7 +104,22 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) FinalizeSignRound( sessionID string, roundContributions []NativeTBTCSignerRoundContribution, ) ([]byte, error) { - return nil, fmt.Errorf("not used") + mbttse.finalizeCalled = true + mbttse.finalizeSessionID = sessionID + mbttse.finalizeInputs = append( + []NativeTBTCSignerRoundContribution{}, + roundContributions..., + ) + + if mbttse.finalizeErr != nil { + return nil, mbttse.finalizeErr + } + + if len(mbttse.finalizeSignature) > 0 { + return append([]byte{}, mbttse.finalizeSignature...), nil + } + + return []byte{0xaa}, nil } func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_ValidatesRequest( @@ -545,7 +584,112 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC } if engine.startCalled { - t.Fatal("did not expect StartSignRound call while coarse finalize flow is unwired") + t.Fatal("did not expect StartSignRound call for non-bootstrap tbtc-signer version") + } + + if engine.finalizeCalled { + t.Fatal("did not expect FinalizeSignRound call for non-bootstrap tbtc-signer version") + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion( + t *testing.T, +) { + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + finalizeSignature: []byte{0xaa}, + } + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":"group-1"}`), + }, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if !engine.runDKGCalled { + t.Fatal("expected RunDKG call in bootstrap tbtc-signer path") + } + + if !engine.startCalled { + t.Fatal("expected StartSignRound call in bootstrap tbtc-signer path") + } + + if engine.startSessionID != "session-1" { + t.Fatalf( + "unexpected StartSignRound session ID\nexpected: [%v]\nactual: [%v]", + "session-1", + engine.startSessionID, + ) + } + + if engine.startKeyGroup != "group-1" { + t.Fatalf( + "unexpected StartSignRound key group\nexpected: [%v]\nactual: [%v]", + "group-1", + engine.startKeyGroup, + ) + } + + if !engine.finalizeCalled { + t.Fatal("expected FinalizeSignRound call in bootstrap tbtc-signer path") + } + + if engine.finalizeSessionID != "session-1" { + t.Fatalf( + "unexpected FinalizeSignRound session ID\nexpected: [%v]\nactual: [%v]", + "session-1", + engine.finalizeSessionID, + ) + } + + if len(engine.finalizeInputs) != 3 { + t.Fatalf( + "unexpected FinalizeSignRound contributions count\nexpected: [%v]\nactual: [%v]", + 3, + len(engine.finalizeInputs), + ) + } + + expectedIdentifiers := []uint16{1, 2, 3} + for i, contribution := range engine.finalizeInputs { + if contribution.Identifier != expectedIdentifiers[i] { + t.Fatalf( + "unexpected contribution identifier at index [%d]\nexpected: [%v]\nactual: [%v]", + i, + expectedIdentifiers[i], + contribution.Identifier, + ) + } + + if len(contribution.Data) == 0 { + t.Fatalf("expected non-empty contribution data at index [%d]", i) + } } } diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index a9baf881ff..fb5568dc6b 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -21,6 +21,7 @@ typedef struct { TbtcBuffer buffer; } TbtcSignerResult; +typedef TbtcSignerResult (*tbtc_version_fn)(void); typedef TbtcSignerResult (*tbtc_run_dkg_fn)( const uint8_t* request_ptr, size_t request_len @@ -43,6 +44,18 @@ static TbtcSignerResult unavailable_tbtc_signer_result(void) { return result; } +static TbtcSignerResult tbtc_signer_version(void) { + tbtc_version_fn version = (tbtc_version_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_version" + ); + if (version == NULL) { + return unavailable_tbtc_signer_result(); + } + + return version(); +} + static TbtcSignerResult tbtc_signer_run_dkg(const uint8_t* request_ptr, size_t request_len) { tbtc_run_dkg_fn run_dkg = (tbtc_run_dkg_fn)dlsym( RTLD_DEFAULT, @@ -158,6 +171,23 @@ func registerBuildTaggedNativeFROSTSigningEngine() error { return RegisterNativeTBTCSignerEngine(&buildTaggedTBTCSignerEngine{}) } +func (bttse *buildTaggedTBTCSignerEngine) Version() (string, error) { + responsePayload, err := callBuildTaggedTBTCSignerVersion() + if err != nil { + return "", err + } + + version := string(responsePayload) + if version == "" { + return "", buildTaggedTBTCSignerOperationError( + "Version", + "response version is empty", + ) + } + + return version, nil +} + func (bttse *buildTaggedTBTCSignerEngine) RunDKG( sessionID string, participants []NativeTBTCSignerDKGParticipant, @@ -525,6 +555,11 @@ func decodeBuildTaggedTBTCSignerFinalizeSignRoundResponse( return signature, nil } +func callBuildTaggedTBTCSignerVersion() ([]byte, error) { + result := C.tbtc_signer_version() + return parseBuildTaggedTBTCSignerResult("Version", result) +} + func callBuildTaggedTBTCSignerRunDKG( requestPayload []byte, ) ([]byte, error) { diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 07e3106b2a..4d7e97e2e8 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -45,6 +45,26 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { if !strings.Contains(err.Error(), "unavailable") { t.Fatalf("unexpected bridge error: [%v]", err) } + + versionedEngine, ok := engine.(interface { + Version() (string, error) + }) + if !ok { + t.Fatal("expected versioned native tbtc-signer engine") + } + + _, err = versionedEngine.Version() + if err == nil { + t.Fatal("expected unavailable tbtc-signer version bridge error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected native cryptography unavailable error: [%v], got [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } } func TestBuildTaggedTBTCSignerRunDKGRequestPayload(t *testing.T) { From b953dc5aacfac96ce10203a3d84a4259e6493993 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 13:50:30 -0600 Subject: [PATCH 52/96] Treat RunDKG key-group as authoritative for legacy scaffold source --- ...ffi_primitive_transitional_frost_native.go | 39 +++- ...rimitive_transitional_frost_native_test.go | 191 ++++++++++++++++++ .../native_tbtc_signer_engine_frost_native.go | 3 + ...resolver_build_frost_native_tbtc_signer.go | 2 +- 4 files changed, 231 insertions(+), 4 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 8396bc6393..5ee6be55a2 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -171,13 +171,17 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) ) } - if payload.KeyGroup != dkgResult.KeyGroup { + keyGroupForRound, err := buildTaggedTBTCSignerRoundKeyGroup( + payload, + dkgResult, + ) + if err != nil { return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, logger, request, legacyPrivateKeyShare, - "tbtc-signer key group does not match RunDKG result", + err.Error(), payload.KeyGroupSource, ) } @@ -225,7 +229,7 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) if err := executeBuildTaggedTBTCSignerBootstrapCoarseRound( request, - payload.KeyGroup, + keyGroupForRound, nativeEngine, ); err != nil { return btlcnnefsp.fallbackTBTCSignerLegacySigning( @@ -304,6 +308,35 @@ func buildTaggedTBTCSignerDKGPlaceholderPublicKeyHex(identifier uint16) string { return fmt.Sprintf("02%04x", identifier) } +func buildTaggedTBTCSignerRoundKeyGroup( + payload *NativeTBTCSignerMaterialPayload, + dkgResult *NativeTBTCSignerDKGResult, +) (string, error) { + if payload == nil { + return "", fmt.Errorf("tbtc-signer payload is nil") + } + + if dkgResult == nil { + return "", fmt.Errorf("tbtc-signer RunDKG result is nil") + } + + if dkgResult.KeyGroup == "" { + return "", fmt.Errorf("tbtc-signer RunDKG key group is empty") + } + + if payload.KeyGroup == dkgResult.KeyGroup { + return payload.KeyGroup, nil + } + + if payload.KeyGroupSource == NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey { + // Scaffold compatibility: legacy-wallet-pubkey key groups are + // placeholder-only and expected to diverge from coarse RunDKG output. + return dkgResult.KeyGroup, nil + } + + return "", fmt.Errorf("tbtc-signer key group does not match RunDKG result") +} + func executeBuildTaggedTBTCSignerBootstrapCoarseRound( request *NativeExecutionFFISigningRequest, keyGroup string, diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 420c886146..d0337794b9 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -518,6 +518,74 @@ func TestBuildTaggedTBTCSignerRunDKGInputs_RejectsInvalidRequest(t *testing.T) { } } +func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { + testCases := []struct { + name string + payload *NativeTBTCSignerMaterialPayload + dkgResult *NativeTBTCSignerDKGResult + expected string + expectError bool + }{ + { + name: "exact match", + payload: &NativeTBTCSignerMaterialPayload{ + KeyGroup: "group-1", + }, + dkgResult: &NativeTBTCSignerDKGResult{ + KeyGroup: "group-1", + }, + expected: "group-1", + }, + { + name: "legacy source mismatch uses dkg key group", + payload: &NativeTBTCSignerMaterialPayload{ + KeyGroup: "legacy-group", + KeyGroupSource: NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + }, + dkgResult: &NativeTBTCSignerDKGResult{ + KeyGroup: "dkg-group", + }, + expected: "dkg-group", + }, + { + name: "non-legacy source mismatch rejects", + payload: &NativeTBTCSignerMaterialPayload{ + KeyGroup: "legacy-group", + KeyGroupSource: "dkg-persisted", + }, + dkgResult: &NativeTBTCSignerDKGResult{ + KeyGroup: "dkg-group", + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := buildTaggedTBTCSignerRoundKeyGroup(tc.payload, tc.dkgResult) + if tc.expectError { + if err == nil { + t.Fatal("expected error") + } + + return + } + + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if actual != tc.expected { + t.Fatalf( + "unexpected key group\nexpected: [%v]\nactual: [%v]", + tc.expected, + actual, + ) + } + }) + } +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath( t *testing.T, ) { @@ -693,6 +761,129 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC } } +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_LegacyKeyGroupSourceUsesRunDKGResult( + t *testing.T, +) { + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + runDKGResult: &NativeTBTCSignerDKGResult{ + SessionID: "session-1", + KeyGroup: "group-from-dkg", + ParticipantCount: 3, + Threshold: 2, + CreatedAtUnix: 1, + }, + finalizeSignature: []byte{0xaa}, + } + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte( + `{"keyGroup":"legacy-wallet-derived","keyGroupSource":"legacy-wallet-pubkey"}`, + ), + }, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if !engine.startCalled { + t.Fatal("expected StartSignRound call in bootstrap path") + } + + if engine.startKeyGroup != "group-from-dkg" { + t.Fatalf( + "unexpected StartSignRound key group\nexpected: [%v]\nactual: [%v]", + "group-from-dkg", + engine.startKeyGroup, + ) + } + + if !engine.finalizeCalled { + t.Fatal("expected FinalizeSignRound call in bootstrap path") + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_KeyGroupMismatchNonLegacySourceSkipsCoarseRound( + t *testing.T, +) { + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + runDKGResult: &NativeTBTCSignerDKGResult{ + SessionID: "session-1", + KeyGroup: "group-from-dkg", + ParticipantCount: 3, + Threshold: 2, + CreatedAtUnix: 1, + }, + } + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte( + `{"keyGroup":"legacy-wallet-derived","keyGroupSource":"dkg-persisted"}`, + ), + }, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if engine.startCalled { + t.Fatal("did not expect StartSignRound call for non-legacy key-group mismatch") + } + + if engine.finalizeCalled { + t.Fatal("did not expect FinalizeSignRound call for non-legacy key-group mismatch") + } +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_NoEngineNoLegacyShare( t *testing.T, ) { diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go index b703d506f5..d9da1472fc 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -8,6 +8,9 @@ const ( // NativeSignerMaterialFormatFrostTBTCSignerV1 carries signer material for // tbtc-signer coarse session APIs. NativeSignerMaterialFormatFrostTBTCSignerV1 = "frost-tbtc-signer-v1" + // NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey marks scaffold-era + // key-group derivation from the legacy wallet public key. + NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey = "legacy-wallet-pubkey" ) // NativeTBTCSignerMaterialPayload is the signer-material payload schema for diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go b/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go index a7e6e81772..ef6a07a252 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go @@ -62,7 +62,7 @@ func (btnsmr *buildTaggedNativeSignerMaterialResolver) ResolveSignerMaterial( // The current value identifies scaffold-era material only. payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ KeyGroup: hex.EncodeToString(keyGroupDigest[:]), - KeyGroupSource: "legacy-wallet-pubkey", + KeyGroupSource: frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, LegacyPrivateKeyShareHex: hex.EncodeToString(legacyPrivateKeySharePayload), }) if err != nil { From 5a9ea5b39ae808e08d57beaaf5d219024ae09f72 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 13:57:43 -0600 Subject: [PATCH 53/96] Make bootstrap synthetic contributions deterministic and round-bound --- ...ffi_primitive_transitional_frost_native.go | 48 +++++-- ...rimitive_transitional_frost_native_test.go | 136 ++++++++++++++++++ 2 files changed, 175 insertions(+), 9 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 5ee6be55a2..00b31508a7 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -4,6 +4,7 @@ package signing import ( "context" + "crypto/sha256" "encoding/hex" "encoding/json" "fmt" @@ -37,6 +38,7 @@ func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} const buildTaggedTBTCSignerBootstrapVersionToken = "bootstrap" +const buildTaggedTBTCSignerSyntheticContributionDomain = "tbtc-signer-bootstrap-contribution-v1" type nativeTBTCSignerVersionedEngine interface { Version() (string, error) @@ -381,9 +383,14 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( return fmt.Errorf("cannot determine included members: [%w]", err) } - roundContributions := buildTaggedTBTCSignerSyntheticRoundContributions( + roundContributions, err := buildTaggedTBTCSignerSyntheticRoundContributions( + roundState, includedMembersIndexes, ) + if err != nil { + return fmt.Errorf("cannot build synthetic round contributions: [%w]", err) + } + if len(roundContributions) < int(roundState.RequiredContributions) { return fmt.Errorf( "insufficient synthetic round contributions: [%v] < [%v]", @@ -408,8 +415,25 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( } func buildTaggedTBTCSignerSyntheticRoundContributions( + roundState *NativeTBTCSignerRoundState, includedMembersIndexes []group.MemberIndex, -) []NativeTBTCSignerRoundContribution { +) ([]NativeTBTCSignerRoundContribution, error) { + if roundState == nil { + return nil, fmt.Errorf("round state is nil") + } + + if roundState.SessionID == "" { + return nil, fmt.Errorf("round state session ID is empty") + } + + if roundState.RoundID == "" { + return nil, fmt.Errorf("round state round ID is empty") + } + + if roundState.MessageDigestHex == "" { + return nil, fmt.Errorf("round state message digest is empty") + } + contributions := make( []NativeTBTCSignerRoundContribution, 0, @@ -418,24 +442,30 @@ func buildTaggedTBTCSignerSyntheticRoundContributions( for _, memberIndex := range includedMembersIndexes { if memberIndex == 0 { - continue + return nil, fmt.Errorf("included member index is zero") } identifier := uint16(memberIndex) + seed := fmt.Sprintf( + "%s:%s:%s:%s:%d", + buildTaggedTBTCSignerSyntheticContributionDomain, + roundState.SessionID, + roundState.RoundID, + roundState.MessageDigestHex, + identifier, + ) + shareDigest := sha256.Sum256([]byte(seed)) + contributions = append( contributions, NativeTBTCSignerRoundContribution{ Identifier: identifier, - Data: []byte{ - byte(identifier >> 8), - byte(identifier), - 0x01, - }, + Data: append([]byte{}, shareDigest[:]...), }, ) } - return contributions + return contributions, nil } func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithLegacyTECDSABridge( diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index d0337794b9..6060ac75ca 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -518,6 +518,142 @@ func TestBuildTaggedTBTCSignerRunDKGInputs_RejectsInvalidRequest(t *testing.T) { } } +func TestBuildTaggedTBTCSignerSyntheticRoundContributions(t *testing.T) { + roundState := &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "round-1", + MessageDigestHex: "aabbccdd", + } + + contributionsFirst, err := buildTaggedTBTCSignerSyntheticRoundContributions( + roundState, + []group.MemberIndex{1, 2, 3}, + ) + if err != nil { + t.Fatalf("unexpected synthetic contribution error: [%v]", err) + } + + contributionsSecond, err := buildTaggedTBTCSignerSyntheticRoundContributions( + roundState, + []group.MemberIndex{1, 2, 3}, + ) + if err != nil { + t.Fatalf("unexpected synthetic contribution error: [%v]", err) + } + + if len(contributionsFirst) != 3 { + t.Fatalf( + "unexpected contribution count\nexpected: [%v]\nactual: [%v]", + 3, + len(contributionsFirst), + ) + } + + expectedIdentifiers := []uint16{1, 2, 3} + for i, contribution := range contributionsFirst { + if contribution.Identifier != expectedIdentifiers[i] { + t.Fatalf( + "unexpected contribution identifier at index [%d]\nexpected: [%v]\nactual: [%v]", + i, + expectedIdentifiers[i], + contribution.Identifier, + ) + } + + if len(contribution.Data) != 32 { + t.Fatalf( + "unexpected contribution size at index [%d]\nexpected: [%v]\nactual: [%v]", + i, + 32, + len(contribution.Data), + ) + } + + if !bytes.Equal(contribution.Data, contributionsSecond[i].Data) { + t.Fatalf("expected deterministic contribution at index [%d]", i) + } + } + + roundStateChanged := &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "round-2", + MessageDigestHex: "aabbccdd", + } + contributionsChanged, err := buildTaggedTBTCSignerSyntheticRoundContributions( + roundStateChanged, + []group.MemberIndex{1, 2, 3}, + ) + if err != nil { + t.Fatalf("unexpected synthetic contribution error: [%v]", err) + } + + if bytes.Equal(contributionsFirst[0].Data, contributionsChanged[0].Data) { + t.Fatal("expected contribution data to change when round metadata changes") + } +} + +func TestBuildTaggedTBTCSignerSyntheticRoundContributions_RejectsInvalidInput(t *testing.T) { + testCases := []struct { + name string + roundState *NativeTBTCSignerRoundState + members []group.MemberIndex + }{ + { + name: "nil round state", + roundState: nil, + members: []group.MemberIndex{1, 2}, + }, + { + name: "empty session id", + roundState: &NativeTBTCSignerRoundState{ + SessionID: "", + RoundID: "round-1", + MessageDigestHex: "aa", + }, + members: []group.MemberIndex{1, 2}, + }, + { + name: "empty round id", + roundState: &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "", + MessageDigestHex: "aa", + }, + members: []group.MemberIndex{1, 2}, + }, + { + name: "empty message digest", + roundState: &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "round-1", + MessageDigestHex: "", + }, + members: []group.MemberIndex{1, 2}, + }, + { + name: "zero member index", + roundState: &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "round-1", + MessageDigestHex: "aa", + }, + members: []group.MemberIndex{0, 2}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := buildTaggedTBTCSignerSyntheticRoundContributions( + tc.roundState, + tc.members, + ) + if err == nil { + t.Fatal("expected error") + } + }) + } +} + func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { testCases := []struct { name string From 8ec68f322cb09de554151c07a5c5857cbb873491 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 14:50:04 -0600 Subject: [PATCH 54/96] Harden bootstrap gate and fallback diagnostics --- ...ffi_primitive_transitional_frost_native.go | 87 +++++-- ...rimitive_transitional_frost_native_test.go | 241 ++++++++++++++++-- 2 files changed, 296 insertions(+), 32 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 00b31508a7..34e513bc53 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -37,7 +37,8 @@ func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( // is wired end-to-end. type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} -const buildTaggedTBTCSignerBootstrapVersionToken = "bootstrap" +const buildTaggedTBTCSignerVersionPrefix = "tbtc-signer/" +const buildTaggedTBTCSignerBootstrapVersionPrerelease = "bootstrap" const buildTaggedTBTCSignerSyntheticContributionDomain = "tbtc-signer-bootstrap-contribution-v1" type nativeTBTCSignerVersionedEngine interface { @@ -146,7 +147,7 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) logger, request, legacyPrivateKeyShare, - "tbtc-signer RunDKG failed", + fmt.Sprintf("tbtc-signer RunDKG failed: [%v]", err), payload.KeyGroupSource, ) } @@ -173,7 +174,7 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) ) } - keyGroupForRound, err := buildTaggedTBTCSignerRoundKeyGroup( + keyGroupForRound, keyGroupSubstituted, err := buildTaggedTBTCSignerRoundKeyGroup( payload, dkgResult, ) @@ -188,6 +189,15 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) ) } + if keyGroupSubstituted && logger != nil { + logger.Debugf( + "substituting scaffold key group from payload source [%s]: payload [%s] -> RunDKG [%s]", + payload.KeyGroupSource, + payload.KeyGroup, + dkgResult.KeyGroup, + ) + } + versionedEngine, isVersioned := nativeEngine.(nativeTBTCSignerVersionedEngine) if !isVersioned { return btlcnnefsp.fallbackTBTCSignerLegacySigning( @@ -207,15 +217,15 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) logger, request, legacyPrivateKeyShare, - "cannot query tbtc-signer version; coarse round scaffold skipped", + fmt.Sprintf( + "cannot query tbtc-signer version; coarse round scaffold skipped: [%v]", + err, + ), payload.KeyGroupSource, ) } - if !strings.Contains( - strings.ToLower(engineVersion), - buildTaggedTBTCSignerBootstrapVersionToken, - ) { + if !isBuildTaggedTBTCSignerBootstrapVersion(engineVersion) { return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, logger, @@ -239,7 +249,7 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) logger, request, legacyPrivateKeyShare, - "tbtc-signer bootstrap coarse round failed", + fmt.Sprintf("tbtc-signer bootstrap coarse round failed: [%v]", err), payload.KeyGroupSource, ) } @@ -313,30 +323,75 @@ func buildTaggedTBTCSignerDKGPlaceholderPublicKeyHex(identifier uint16) string { func buildTaggedTBTCSignerRoundKeyGroup( payload *NativeTBTCSignerMaterialPayload, dkgResult *NativeTBTCSignerDKGResult, -) (string, error) { +) (string, bool, error) { if payload == nil { - return "", fmt.Errorf("tbtc-signer payload is nil") + return "", false, fmt.Errorf("tbtc-signer payload is nil") } if dkgResult == nil { - return "", fmt.Errorf("tbtc-signer RunDKG result is nil") + return "", false, fmt.Errorf("tbtc-signer RunDKG result is nil") } if dkgResult.KeyGroup == "" { - return "", fmt.Errorf("tbtc-signer RunDKG key group is empty") + return "", false, fmt.Errorf("tbtc-signer RunDKG key group is empty") } if payload.KeyGroup == dkgResult.KeyGroup { - return payload.KeyGroup, nil + return payload.KeyGroup, false, nil } if payload.KeyGroupSource == NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey { // Scaffold compatibility: legacy-wallet-pubkey key groups are // placeholder-only and expected to diverge from coarse RunDKG output. - return dkgResult.KeyGroup, nil + return dkgResult.KeyGroup, true, nil + } + + return "", false, fmt.Errorf("tbtc-signer key group does not match RunDKG result") +} + +func isBuildTaggedTBTCSignerBootstrapVersion(version string) bool { + version = strings.TrimSpace(version) + if !strings.HasPrefix(version, buildTaggedTBTCSignerVersionPrefix) { + return false + } + + version = strings.TrimPrefix(version, buildTaggedTBTCSignerVersionPrefix) + coreVersion, prerelease, hasPrerelease := strings.Cut(version, "-") + if !hasPrerelease { + return false + } + + if prerelease != buildTaggedTBTCSignerBootstrapVersionPrerelease && + !strings.HasPrefix( + prerelease, + buildTaggedTBTCSignerBootstrapVersionPrerelease+".", + ) { + return false + } + + coreSegments := strings.Split(coreVersion, ".") + if len(coreSegments) != 3 { + return false + } + + // Bootstrap scaffold must be enabled only on 0.x.y pre-release builds. + if coreSegments[0] != "0" { + return false + } + + for _, segment := range coreSegments { + if segment == "" { + return false + } + + for _, character := range segment { + if character < '0' || character > '9' { + return false + } + } } - return "", fmt.Errorf("tbtc-signer key group does not match RunDKG result") + return true } func executeBuildTaggedTBTCSignerBootstrapCoarseRound( diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 6060ac75ca..b41e48baea 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -7,6 +7,8 @@ import ( "encoding/hex" "errors" "math/big" + "reflect" + "strings" "testing" "github.com/keep-network/keep-core/pkg/internal/tecdsatest" @@ -21,19 +23,24 @@ type mockBuildTaggedTBTCSignerEngine struct { runDKGThreshold uint16 runDKGResult *NativeTBTCSignerDKGResult runDKGErr error - version string - versionErr error - startCalled bool - startSessionID string - startMessage []byte - startKeyGroup string - startRoundState *NativeTBTCSignerRoundState - startErr error - finalizeCalled bool - finalizeSessionID string - finalizeInputs []NativeTBTCSignerRoundContribution - finalizeSignature []byte - finalizeErr error + runDKGFn func( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + ) (*NativeTBTCSignerDKGResult, error) + version string + versionErr error + startCalled bool + startSessionID string + startMessage []byte + startKeyGroup string + startRoundState *NativeTBTCSignerRoundState + startErr error + finalizeCalled bool + finalizeSessionID string + finalizeInputs []NativeTBTCSignerRoundContribution + finalizeSignature []byte + finalizeErr error } func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( @@ -53,6 +60,10 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( return nil, mbttse.runDKGErr } + if mbttse.runDKGFn != nil { + return mbttse.runDKGFn(sessionID, participants, threshold) + } + if mbttse.runDKGResult != nil { return mbttse.runDKGResult, nil } @@ -660,6 +671,7 @@ func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { payload *NativeTBTCSignerMaterialPayload dkgResult *NativeTBTCSignerDKGResult expected string + substituted bool expectError bool }{ { @@ -670,7 +682,8 @@ func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { dkgResult: &NativeTBTCSignerDKGResult{ KeyGroup: "group-1", }, - expected: "group-1", + expected: "group-1", + substituted: false, }, { name: "legacy source mismatch uses dkg key group", @@ -681,7 +694,8 @@ func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { dkgResult: &NativeTBTCSignerDKGResult{ KeyGroup: "dkg-group", }, - expected: "dkg-group", + expected: "dkg-group", + substituted: true, }, { name: "non-legacy source mismatch rejects", @@ -698,7 +712,7 @@ func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - actual, err := buildTaggedTBTCSignerRoundKeyGroup(tc.payload, tc.dkgResult) + actual, substituted, err := buildTaggedTBTCSignerRoundKeyGroup(tc.payload, tc.dkgResult) if tc.expectError { if err == nil { t.Fatal("expected error") @@ -718,6 +732,82 @@ func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { actual, ) } + + if substituted != tc.substituted { + t.Fatalf( + "unexpected substitution flag\nexpected: [%v]\nactual: [%v]", + tc.substituted, + substituted, + ) + } + }) + } +} + +func TestIsBuildTaggedTBTCSignerBootstrapVersion(t *testing.T) { + testCases := []struct { + name string + version string + expected bool + }{ + { + name: "valid exact bootstrap", + version: "tbtc-signer/0.1.0-bootstrap", + expected: true, + }, + { + name: "valid bootstrap dotted suffix", + version: "tbtc-signer/0.1.0-bootstrap.1", + expected: true, + }, + { + name: "invalid non-bootstrap prerelease", + version: "tbtc-signer/0.1.0-post-bootstrap", + expected: false, + }, + { + name: "invalid major version one", + version: "tbtc-signer/1.0.0-bootstrap", + expected: false, + }, + { + name: "invalid missing prerelease", + version: "tbtc-signer/0.1.0", + expected: false, + }, + { + name: "invalid malformed core semver", + version: "tbtc-signer/0.1-bootstrap", + expected: false, + }, + { + name: "invalid prefix", + version: "other/0.1.0-bootstrap", + expected: false, + }, + { + name: "invalid uppercase bootstrap token", + version: "tbtc-signer/0.1.0-Bootstrap", + expected: false, + }, + { + name: "invalid substring trap", + version: "tbtc-signer/0.1.0-post-bootstrap-cleanup", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := isBuildTaggedTBTCSignerBootstrapVersion(tc.version) + if actual != tc.expected { + t.Fatalf( + "unexpected bootstrap version classification\nversion: [%s]\nexpected: [%v]\nactual: [%v]", + tc.version, + tc.expected, + actual, + ) + } }) } } @@ -1089,3 +1179,122 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC t.Fatal("expected fallback event without legacy private key share") } } + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_AttemptVariationRunDKGConflictFallsBack( + t *testing.T, +) { + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + var firstParticipants []NativeTBTCSignerDKGParticipant + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0", + runDKGFn: func( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + ) (*NativeTBTCSignerDKGResult, error) { + if firstParticipants == nil { + firstParticipants = append( + []NativeTBTCSignerDKGParticipant{}, + participants..., + ) + + return &NativeTBTCSignerDKGResult{ + SessionID: sessionID, + KeyGroup: "group-1", + ParticipantCount: uint16(len(participants)), + Threshold: threshold, + CreatedAtUnix: 1, + }, nil + } + + if !reflect.DeepEqual(participants, firstParticipants) { + return nil, errors.New("session_conflict") + } + + return &NativeTBTCSignerDKGResult{ + SessionID: sessionID, + KeyGroup: "group-1", + ParticipantCount: uint16(len(participants)), + Threshold: threshold, + CreatedAtUnix: 1, + }, nil + }, + } + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + var observedEvents []NativeTBTCSignerFallbackEvent + err = RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + observedEvents = append(observedEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected observer registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + baseRequest := &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":"group-1","keyGroupSource":"legacy-wallet-pubkey"}`), + }, + } + + _, err = primitive.Sign(nil, nil, baseRequest) + if err == nil { + t.Fatal("expected first signing error due to legacy fallback without private key share") + } + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected first signing error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + secondRequest := *baseRequest + secondRequest.Attempt = &Attempt{ + ExcludedMembersIndexes: []group.MemberIndex{3}, + } + + _, err = primitive.Sign(nil, nil, &secondRequest) + if err == nil { + t.Fatal("expected second signing error due to legacy fallback without private key share") + } + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected second signing error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if len(observedEvents) != 2 { + t.Fatalf( + "unexpected fallback event count\nexpected: [%d]\nactual: [%d]", + 2, + len(observedEvents), + ) + } + + if !strings.Contains(observedEvents[1].Reason, "session_conflict") { + t.Fatalf( + "expected second fallback reason to include session_conflict\nactual: [%s]", + observedEvents[1].Reason, + ) + } +} From 1c36eb57ffc3521dbe3ddd1a98458a28b4d8c42f Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 15:31:15 -0600 Subject: [PATCH 55/96] Plumb tbtc-signer bootstrap contributions over channel --- ...ffi_primitive_transitional_frost_native.go | 235 +++++++++++++++++- ...rimitive_transitional_frost_native_test.go | 178 +++++++++++++ 2 files changed, 405 insertions(+), 8 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 34e513bc53..83710d8b67 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -40,11 +40,59 @@ type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} const buildTaggedTBTCSignerVersionPrefix = "tbtc-signer/" const buildTaggedTBTCSignerBootstrapVersionPrerelease = "bootstrap" const buildTaggedTBTCSignerSyntheticContributionDomain = "tbtc-signer-bootstrap-contribution-v1" +const buildTaggedTBTCSignerMessageTypePrefix = "frost_signing/native_tbtc_signer/" type nativeTBTCSignerVersionedEngine interface { Version() (string, error) } +type buildTaggedTBTCSignerRoundContributionMessage struct { + SenderIDValue uint32 `json:"senderID"` + SessionIDValue string `json:"sessionID"` + ContributionIdentifier uint16 `json:"contributionIdentifier"` + ContributionData []byte `json:"contributionData"` +} + +func (bttsrcm *buildTaggedTBTCSignerRoundContributionMessage) SenderID() group.MemberIndex { + return group.MemberIndex(bttsrcm.SenderIDValue) +} + +func (bttsrcm *buildTaggedTBTCSignerRoundContributionMessage) SessionID() string { + return bttsrcm.SessionIDValue +} + +func (bttsrcm *buildTaggedTBTCSignerRoundContributionMessage) Type() string { + return buildTaggedTBTCSignerMessageTypePrefix + "round_contribution" +} + +func (bttsrcm *buildTaggedTBTCSignerRoundContributionMessage) Marshal() ([]byte, error) { + return json.Marshal(bttsrcm) +} + +func (bttsrcm *buildTaggedTBTCSignerRoundContributionMessage) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, bttsrcm); err != nil { + return err + } + + if bttsrcm.SenderID() == 0 { + return fmt.Errorf("sender ID is zero") + } + + if bttsrcm.SessionID() == "" { + return fmt.Errorf("session ID is empty") + } + + if bttsrcm.ContributionIdentifier == 0 { + return fmt.Errorf("contribution identifier is zero") + } + + if len(bttsrcm.ContributionData) == 0 { + return fmt.Errorf("contribution data is empty") + } + + return nil +} + func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) Sign( ctx context.Context, logger log.StandardLogger, @@ -240,6 +288,7 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) } if err := executeBuildTaggedTBTCSignerBootstrapCoarseRound( + ctx, request, keyGroupForRound, nativeEngine, @@ -395,6 +444,7 @@ func isBuildTaggedTBTCSignerBootstrapVersion(version string) bool { } func executeBuildTaggedTBTCSignerBootstrapCoarseRound( + ctx context.Context, request *NativeExecutionFFISigningRequest, keyGroup string, nativeEngine NativeTBTCSignerEngine, @@ -411,6 +461,22 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( return fmt.Errorf("native tbtc-signer engine is nil") } + if ctx == nil { + ctx = context.Background() + } + + includedMembersSet, includedMembersIndexes, err := includedMembersFromRequest(request) + if err != nil { + return fmt.Errorf("cannot determine included members: [%w]", err) + } + + if _, ok := includedMembersSet[request.MemberIndex]; !ok { + return fmt.Errorf( + "member [%v] not included in tbtc-signer signing attempt", + request.MemberIndex, + ) + } + messageBytes := request.Message.Bytes() if len(messageBytes) == 0 { messageBytes = []byte{0} @@ -433,22 +499,20 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( return fmt.Errorf("start sign round required contributions are zero") } - _, includedMembersIndexes, err := includedMembersFromRequest(request) - if err != nil { - return fmt.Errorf("cannot determine included members: [%w]", err) - } - - roundContributions, err := buildTaggedTBTCSignerSyntheticRoundContributions( + roundContributions, err := buildTaggedTBTCSignerRoundContributions( + ctx, + request, roundState, + includedMembersSet, includedMembersIndexes, ) if err != nil { - return fmt.Errorf("cannot build synthetic round contributions: [%w]", err) + return fmt.Errorf("cannot collect round contributions: [%w]", err) } if len(roundContributions) < int(roundState.RequiredContributions) { return fmt.Errorf( - "insufficient synthetic round contributions: [%v] < [%v]", + "insufficient round contributions: [%v] < [%v]", len(roundContributions), roundState.RequiredContributions, ) @@ -469,6 +533,154 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( return nil } +func buildTaggedTBTCSignerRoundContributions( + ctx context.Context, + request *NativeExecutionFFISigningRequest, + roundState *NativeTBTCSignerRoundState, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, +) ([]NativeTBTCSignerRoundContribution, error) { + if request == nil { + return nil, fmt.Errorf("request is nil") + } + + if request.Channel == nil { + // Compatibility path for unit tests that do not attach a broadcast + // channel. Runtime signer flows provide a channel and use contribution + // exchange with peers. + return buildTaggedTBTCSignerSyntheticRoundContributions( + roundState, + includedMembersIndexes, + ) + } + + ownContributions, err := buildTaggedTBTCSignerSyntheticRoundContributions( + roundState, + []group.MemberIndex{request.MemberIndex}, + ) + if err != nil { + return nil, fmt.Errorf("cannot build own round contribution: [%w]", err) + } + + if len(ownContributions) != 1 { + return nil, fmt.Errorf("unexpected own contribution count: [%v]", len(ownContributions)) + } + + ownContribution := ownContributions[0] + + roundContributionMessage := &buildTaggedTBTCSignerRoundContributionMessage{ + SenderIDValue: uint32(request.MemberIndex), + SessionIDValue: request.SessionID, + ContributionIdentifier: ownContribution.Identifier, + ContributionData: append([]byte{}, ownContribution.Data...), + } + + if err := request.Channel.Send( + ctx, + roundContributionMessage, + net.BackoffRetransmissionStrategy, + ); err != nil { + return nil, fmt.Errorf("cannot send round contribution message: [%w]", err) + } + + peerMessages, err := collectBuildTaggedTBTCSignerRoundContributionMessages( + ctx, + request, + includedMembersSet, + includedMembersIndexes, + ) + if err != nil { + return nil, err + } + + contributionsBySender := map[group.MemberIndex]NativeTBTCSignerRoundContribution{ + request.MemberIndex: ownContribution, + } + + for senderID, message := range peerMessages { + contributionsBySender[senderID] = NativeTBTCSignerRoundContribution{ + Identifier: message.ContributionIdentifier, + Data: append([]byte{}, message.ContributionData...), + } + } + + orderedContributions := make( + []NativeTBTCSignerRoundContribution, + 0, + len(includedMembersIndexes), + ) + for _, memberIndex := range includedMembersIndexes { + contribution, ok := contributionsBySender[memberIndex] + if !ok { + return nil, fmt.Errorf("missing contribution from member [%v]", memberIndex) + } + + orderedContributions = append(orderedContributions, contribution) + } + + return orderedContributions, nil +} + +func collectBuildTaggedTBTCSignerRoundContributionMessages( + ctx context.Context, + request *NativeExecutionFFISigningRequest, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, +) (map[group.MemberIndex]*buildTaggedTBTCSignerRoundContributionMessage, error) { + expectedMessagesCount := len(includedMembersIndexes) - 1 + if expectedMessagesCount <= 0 { + return map[group.MemberIndex]*buildTaggedTBTCSignerRoundContributionMessage{}, nil + } + + recvCtx, cancelRecvCtx := context.WithCancel(ctx) + defer cancelRecvCtx() + + messageChan := make( + chan *buildTaggedTBTCSignerRoundContributionMessage, + expectedMessagesCount*4+1, + ) + + request.Channel.Recv(recvCtx, func(message net.Message) { + payload, ok := message.Payload().(*buildTaggedTBTCSignerRoundContributionMessage) + if !ok { + return + } + + if !shouldAcceptNativeFROSTMessage( + request, + includedMembersSet, + payload.SenderID(), + payload.SessionID(), + message.SenderPublicKey(), + ) { + return + } + + select { + case messageChan <- payload: + default: + } + }) + + receivedMessages := make( + map[group.MemberIndex]*buildTaggedTBTCSignerRoundContributionMessage, + ) + for len(receivedMessages) < expectedMessagesCount { + select { + case <-ctx.Done(): + return nil, fmt.Errorf( + "tbtc-signer round contribution collection interrupted: [%w]", + ctx.Err(), + ) + + case message := <-messageChan: + receivedMessages[message.SenderID()] = message + } + } + + return receivedMessages, nil +} + func buildTaggedTBTCSignerSyntheticRoundContributions( roundState *NativeTBTCSignerRoundState, includedMembersIndexes []group.MemberIndex, @@ -617,10 +829,17 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( channel net.BroadcastChannel, ) { + registerBuildTaggedTBTCSignerUnmarshallers(channel) registerNativeFROSTSigningUnmarshallers(channel) legacySigning.RegisterUnmarshallers(channel) } +func registerBuildTaggedTBTCSignerUnmarshallers(channel net.BroadcastChannel) { + channel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &buildTaggedTBTCSignerRoundContributionMessage{} + }) +} + func decodeBuildTaggedLegacyPrivateKeyShare( signerMaterial *NativeSignerMaterial, ) (*tecdsa.PrivateKeyShare, error) { diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index b41e48baea..7516e1031e 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -4,14 +4,18 @@ package signing import ( "bytes" + "context" "encoding/hex" "errors" "math/big" "reflect" "strings" + "sync" "testing" + "time" "github.com/keep-network/keep-core/pkg/internal/tecdsatest" + "github.com/keep-network/keep-core/pkg/net/local" "github.com/keep-network/keep-core/pkg/protocol/group" "github.com/keep-network/keep-core/pkg/tecdsa" ) @@ -133,6 +137,67 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) FinalizeSignRound( return []byte{0xaa}, nil } +type deterministicBuildTaggedTBTCSignerBootstrapRoundEngine struct { + roundState *NativeTBTCSignerRoundState + finalizeMutex sync.Mutex + finalizeCalls int + finalizeInput []NativeTBTCSignerRoundContribution +} + +func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) RunDKG( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, +) (*NativeTBTCSignerDKGResult, error) { + return &NativeTBTCSignerDKGResult{ + SessionID: sessionID, + KeyGroup: "group-1", + ParticipantCount: uint16(len(participants)), + Threshold: threshold, + CreatedAtUnix: 1, + }, nil +} + +func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) StartSignRound( + sessionID string, + _ []byte, + _ string, +) (*NativeTBTCSignerRoundState, error) { + if dbttsbre.roundState != nil { + return dbttsbre.roundState, nil + } + + return &NativeTBTCSignerRoundState{ + SessionID: sessionID, + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "00", + }, nil +} + +func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) FinalizeSignRound( + _ string, + roundContributions []NativeTBTCSignerRoundContribution, +) ([]byte, error) { + dbttsbre.finalizeMutex.Lock() + defer dbttsbre.finalizeMutex.Unlock() + + dbttsbre.finalizeCalls++ + dbttsbre.finalizeInput = append( + []NativeTBTCSignerRoundContribution{}, + roundContributions..., + ) + + return []byte{0xaa}, nil +} + +func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) finalizeInputs() []NativeTBTCSignerRoundContribution { + dbttsbre.finalizeMutex.Lock() + defer dbttsbre.finalizeMutex.Unlock() + + return append([]NativeTBTCSignerRoundContribution{}, dbttsbre.finalizeInput...) +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_ValidatesRequest( t *testing.T, ) { @@ -665,6 +730,119 @@ func TestBuildTaggedTBTCSignerSyntheticRoundContributions_RejectsInvalidInput(t } } +func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_ExchangesContributionsOverChannel( + t *testing.T, +) { + provider := local.Connect() + channel, err := provider.BroadcastChannelFor("tbtc-signer-bootstrap-round-plumbing-test") + if err != nil { + t.Fatalf("failed creating broadcast channel: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + primitive.RegisterUnmarshallers(channel) + + roundState := &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "0011", + } + + engineByMember := map[group.MemberIndex]*deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{ + 1: &deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{roundState: roundState}, + 2: &deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{roundState: roundState}, + } + + requestByMember := map[group.MemberIndex]*NativeExecutionFFISigningRequest{ + 1: { + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 2, + DishonestThreshold: 1, + Channel: channel, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + }, + 2: { + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 2, + GroupSize: 2, + DishonestThreshold: 1, + Channel: channel, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + }, + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelCtx() + + var wg sync.WaitGroup + signingErrors := make(chan error, len(requestByMember)) + + for memberIndex, request := range requestByMember { + engine := engineByMember[memberIndex] + wg.Add(1) + + go func( + signingRequest *NativeExecutionFFISigningRequest, + signingEngine NativeTBTCSignerEngine, + ) { + defer wg.Done() + + signingErrors <- executeBuildTaggedTBTCSignerBootstrapCoarseRound( + ctx, + signingRequest, + "group-1", + signingEngine, + ) + }(request, engine) + } + + wg.Wait() + close(signingErrors) + + for signingErr := range signingErrors { + if signingErr != nil { + t.Fatalf("unexpected signing error: [%v]", signingErr) + } + } + + for memberIndex, engine := range engineByMember { + finalizeInputs := engine.finalizeInputs() + if len(finalizeInputs) != 2 { + t.Fatalf( + "unexpected finalize input count for member [%v]\nexpected: [%v]\nactual: [%v]", + memberIndex, + 2, + len(finalizeInputs), + ) + } + + if finalizeInputs[0].Identifier != 1 || finalizeInputs[1].Identifier != 2 { + t.Fatalf( + "unexpected finalize identifiers for member [%v]\nexpected: [1 2]\nactual: [%v %v]", + memberIndex, + finalizeInputs[0].Identifier, + finalizeInputs[1].Identifier, + ) + } + + if len(finalizeInputs[0].Data) == 0 || len(finalizeInputs[1].Data) == 0 { + t.Fatalf("expected non-empty finalize contribution data for member [%v]", memberIndex) + } + } +} + func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { testCases := []struct { name string From f6e1943105defc02760b707aec60dfb8cea06b34 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 15:42:58 -0600 Subject: [PATCH 56/96] Plumb member-scoped StartSignRound contributions --- ...ffi_primitive_transitional_frost_native.go | 73 ++++++++++++++++-- ...rimitive_transitional_frost_native_test.go | 74 ++++++++++++++++--- ...e_tbtc_signer_registration_frost_native.go | 69 ++++++++++++++--- ...c_signer_registration_frost_native_test.go | 61 ++++++++++++++- .../native_tbtc_signer_engine_frost_native.go | 2 + ...ve_tbtc_signer_engine_frost_native_test.go | 2 + 6 files changed, 253 insertions(+), 28 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 83710d8b67..1787ddb16e 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -482,8 +482,13 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( messageBytes = []byte{0} } + if request.MemberIndex == 0 { + return fmt.Errorf("request member index is zero") + } + roundState, err := nativeEngine.StartSignRound( request.SessionID, + uint16(request.MemberIndex), messageBytes, keyGroup, ) @@ -554,20 +559,14 @@ func buildTaggedTBTCSignerRoundContributions( ) } - ownContributions, err := buildTaggedTBTCSignerSyntheticRoundContributions( + ownContribution, err := buildTaggedTBTCSignerOwnRoundContribution( + request, roundState, - []group.MemberIndex{request.MemberIndex}, ) if err != nil { return nil, fmt.Errorf("cannot build own round contribution: [%w]", err) } - if len(ownContributions) != 1 { - return nil, fmt.Errorf("unexpected own contribution count: [%v]", len(ownContributions)) - } - - ownContribution := ownContributions[0] - roundContributionMessage := &buildTaggedTBTCSignerRoundContributionMessage{ SenderIDValue: uint32(request.MemberIndex), SessionIDValue: request.SessionID, @@ -621,6 +620,64 @@ func buildTaggedTBTCSignerRoundContributions( return orderedContributions, nil } +func buildTaggedTBTCSignerOwnRoundContribution( + request *NativeExecutionFFISigningRequest, + roundState *NativeTBTCSignerRoundState, +) (NativeTBTCSignerRoundContribution, error) { + if request == nil { + return NativeTBTCSignerRoundContribution{}, fmt.Errorf("request is nil") + } + + if request.MemberIndex == 0 { + return NativeTBTCSignerRoundContribution{}, fmt.Errorf("request member index is zero") + } + + if roundState != nil && roundState.OwnContribution != nil { + ownContribution := roundState.OwnContribution + if ownContribution.Identifier == 0 { + return NativeTBTCSignerRoundContribution{}, fmt.Errorf( + "round state own contribution identifier is zero", + ) + } + + if len(ownContribution.Data) == 0 { + return NativeTBTCSignerRoundContribution{}, fmt.Errorf( + "round state own contribution data is empty", + ) + } + + if ownContribution.Identifier != uint16(request.MemberIndex) { + return NativeTBTCSignerRoundContribution{}, fmt.Errorf( + "round state own contribution identifier [%v] does not match member index [%v]", + ownContribution.Identifier, + request.MemberIndex, + ) + } + + return NativeTBTCSignerRoundContribution{ + Identifier: ownContribution.Identifier, + Data: append([]byte{}, ownContribution.Data...), + }, nil + } + + ownContributions, err := buildTaggedTBTCSignerSyntheticRoundContributions( + roundState, + []group.MemberIndex{request.MemberIndex}, + ) + if err != nil { + return NativeTBTCSignerRoundContribution{}, err + } + + if len(ownContributions) != 1 { + return NativeTBTCSignerRoundContribution{}, fmt.Errorf( + "unexpected own contribution count: [%v]", + len(ownContributions), + ) + } + + return ownContributions[0], nil +} + func collectBuildTaggedTBTCSignerRoundContributionMessages( ctx context.Context, request *NativeExecutionFFISigningRequest, diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 7516e1031e..71158bd0c6 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -36,6 +36,7 @@ type mockBuildTaggedTBTCSignerEngine struct { versionErr error startCalled bool startSessionID string + startMemberID uint16 startMessage []byte startKeyGroup string startRoundState *NativeTBTCSignerRoundState @@ -91,11 +92,13 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) Version() (string, error) { func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( sessionID string, + memberIdentifier uint16, message []byte, keyGroup string, ) (*NativeTBTCSignerRoundState, error) { mbttse.startCalled = true mbttse.startSessionID = sessionID + mbttse.startMemberID = memberIdentifier mbttse.startMessage = append([]byte{}, message...) mbttse.startKeyGroup = keyGroup @@ -160,10 +163,18 @@ func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) RunDKG( func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) StartSignRound( sessionID string, + memberIdentifier uint16, _ []byte, _ string, ) (*NativeTBTCSignerRoundState, error) { if dbttsbre.roundState != nil { + if dbttsbre.roundState.OwnContribution == nil { + dbttsbre.roundState.OwnContribution = &NativeTBTCSignerRoundContribution{ + Identifier: memberIdentifier, + Data: []byte{byte(memberIdentifier), 0xab}, + } + } + return dbttsbre.roundState, nil } @@ -172,6 +183,10 @@ func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) StartSig RoundID: "round-1", RequiredContributions: 2, MessageDigestHex: "00", + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: memberIdentifier, + Data: []byte{byte(memberIdentifier), 0xab}, + }, }, nil } @@ -742,16 +757,31 @@ func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_ExchangesContributions primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} primitive.RegisterUnmarshallers(channel) - roundState := &NativeTBTCSignerRoundState{ - SessionID: "session-1", - RoundID: "round-1", - RequiredContributions: 2, - MessageDigestHex: "0011", - } - engineByMember := map[group.MemberIndex]*deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{ - 1: &deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{roundState: roundState}, - 2: &deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{roundState: roundState}, + 1: &deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{ + roundState: &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "0011", + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: 1, + Data: []byte{0x11, 0x01}, + }, + }, + }, + 2: &deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{ + roundState: &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "0011", + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: 2, + Data: []byte{0x22, 0x02}, + }, + }, + }, } requestByMember := map[group.MemberIndex]*NativeExecutionFFISigningRequest{ @@ -840,6 +870,24 @@ func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_ExchangesContributions if len(finalizeInputs[0].Data) == 0 || len(finalizeInputs[1].Data) == 0 { t.Fatalf("expected non-empty finalize contribution data for member [%v]", memberIndex) } + + if !bytes.Equal(finalizeInputs[0].Data, []byte{0x11, 0x01}) { + t.Fatalf( + "unexpected contribution data for identifier 1, member [%v]\nexpected: [%x]\nactual: [%x]", + memberIndex, + []byte{0x11, 0x01}, + finalizeInputs[0].Data, + ) + } + + if !bytes.Equal(finalizeInputs[1].Data, []byte{0x22, 0x02}) { + t.Fatalf( + "unexpected contribution data for identifier 2, member [%v]\nexpected: [%x]\nactual: [%x]", + memberIndex, + []byte{0x22, 0x02}, + finalizeInputs[1].Data, + ) + } } } @@ -1120,6 +1168,14 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } + if engine.startMemberID != 1 { + t.Fatalf( + "unexpected StartSignRound member identifier\nexpected: [%v]\nactual: [%v]", + 1, + engine.startMemberID, + ) + } + if engine.startKeyGroup != "group-1" { t.Fatalf( "unexpected StartSignRound key group\nexpected: [%v]\nactual: [%v]", diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index fb5568dc6b..e8497cff2d 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -137,16 +137,18 @@ type buildTaggedTBTCSignerRunDKGResponse struct { } type buildTaggedTBTCSignerStartSignRoundRequest struct { - SessionID string `json:"session_id"` - MessageHex string `json:"message_hex"` - KeyGroup string `json:"key_group"` + SessionID string `json:"session_id"` + MemberIdentifier uint16 `json:"member_identifier"` + MessageHex string `json:"message_hex"` + KeyGroup string `json:"key_group"` } type buildTaggedTBTCSignerStartSignRoundResponse struct { - SessionID string `json:"session_id"` - RoundID string `json:"round_id"` - RequiredContributions uint16 `json:"required_contributions"` - MessageDigestHex string `json:"message_digest_hex"` + SessionID string `json:"session_id"` + RoundID string `json:"round_id"` + RequiredContributions uint16 `json:"required_contributions"` + MessageDigestHex string `json:"message_digest_hex"` + OwnContribution *buildTaggedTBTCSignerFinalizeRoundContribution `json:"own_contribution"` } type buildTaggedTBTCSignerFinalizeSignRoundRequest struct { @@ -212,11 +214,13 @@ func (bttse *buildTaggedTBTCSignerEngine) RunDKG( func (bttse *buildTaggedTBTCSignerEngine) StartSignRound( sessionID string, + memberIdentifier uint16, message []byte, keyGroup string, ) (*NativeTBTCSignerRoundState, error) { requestPayload, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( sessionID, + memberIdentifier, message, keyGroup, ) @@ -395,6 +399,7 @@ func decodeBuildTaggedTBTCSignerRunDKGResponse( func buildTaggedTBTCSignerStartSignRoundRequestPayload( sessionID string, + memberIdentifier uint16, message []byte, keyGroup string, ) ([]byte, error) { @@ -412,10 +417,18 @@ func buildTaggedTBTCSignerStartSignRoundRequestPayload( ) } + if memberIdentifier == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "member identifier is zero", + ) + } + request := buildTaggedTBTCSignerStartSignRoundRequest{ - SessionID: sessionID, - MessageHex: hex.EncodeToString(message), - KeyGroup: keyGroup, + SessionID: sessionID, + MemberIdentifier: memberIdentifier, + MessageHex: hex.EncodeToString(message), + KeyGroup: keyGroup, } payload, err := json.Marshal(request) @@ -461,11 +474,47 @@ func decodeBuildTaggedTBTCSignerStartSignRoundResponse( ) } + var ownContribution *NativeTBTCSignerRoundContribution + if response.OwnContribution != nil { + if response.OwnContribution.Identifier == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "response own contribution identifier is zero", + ) + } + + if response.OwnContribution.SignatureShareHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "response own contribution signature share is empty", + ) + } + + ownContributionData, err := hex.DecodeString( + response.OwnContribution.SignatureShareHex, + ) + if err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + fmt.Sprintf( + "response own contribution signature share is invalid hex: %v", + err, + ), + ) + } + + ownContribution = &NativeTBTCSignerRoundContribution{ + Identifier: response.OwnContribution.Identifier, + Data: ownContributionData, + } + } + return &NativeTBTCSignerRoundState{ SessionID: response.SessionID, RoundID: response.RoundID, RequiredContributions: response.RequiredContributions, MessageDigestHex: response.MessageDigestHex, + OwnContribution: ownContribution, }, nil } diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 4d7e97e2e8..f3f9fb1497 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -27,6 +27,7 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { _, err = engine.StartSignRound( "session-1", + 1, []byte("message"), "key-group", ) @@ -253,6 +254,7 @@ func TestDecodeBuildTaggedTBTCSignerRunDKGResponse(t *testing.T) { func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload(t *testing.T) { payload, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( "session-1", + 3, []byte{0xab, 0xcd}, "key-group-1", ) @@ -288,11 +290,36 @@ func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload(t *testing.T) { request.KeyGroup, ) } + + if request.MemberIdentifier != 3 { + t.Fatalf( + "unexpected member identifier\nexpected: [%v]\nactual: [%v]", + 3, + request.MemberIdentifier, + ) + } } func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_EmptySessionID(t *testing.T) { _, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( "", + 1, + []byte{0xab}, + "key-group-1", + ) + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected native cryptography unavailable error: [%v], got [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } +} + +func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_ZeroMemberID(t *testing.T) { + _, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( + "session-1", + 0, []byte{0xab}, "key-group-1", ) @@ -360,7 +387,7 @@ func TestBuildTaggedTBTCSignerFinalizeSignRoundRequestPayload(t *testing.T) { func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse(t *testing.T) { roundState, err := decodeBuildTaggedTBTCSignerStartSignRoundResponse( []byte( - `{"session_id":"session-1","round_id":"round-1","required_contributions":2,"message_digest_hex":"abcd"}`, + `{"session_id":"session-1","round_id":"round-1","required_contributions":2,"message_digest_hex":"abcd","own_contribution":{"identifier":3,"signature_share_hex":"deadbeef"}}`, ), ) if err != nil { @@ -398,6 +425,38 @@ func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse(t *testing.T) { roundState.MessageDigestHex, ) } + + if roundState.OwnContribution == nil { + t.Fatal("expected own contribution in round state response") + } + + if roundState.OwnContribution.Identifier != 3 { + t.Fatalf( + "unexpected own contribution identifier\nexpected: [%v]\nactual: [%v]", + 3, + roundState.OwnContribution.Identifier, + ) + } + + expectedOwnContributionData := []byte{0xde, 0xad, 0xbe, 0xef} + if len(roundState.OwnContribution.Data) != len(expectedOwnContributionData) { + t.Fatalf( + "unexpected own contribution data length\nexpected: [%v]\nactual: [%v]", + len(expectedOwnContributionData), + len(roundState.OwnContribution.Data), + ) + } + + for i := range roundState.OwnContribution.Data { + if roundState.OwnContribution.Data[i] != expectedOwnContributionData[i] { + t.Fatalf( + "unexpected own contribution byte at index [%d]\nexpected: [%x]\nactual: [%x]", + i, + expectedOwnContributionData[i], + roundState.OwnContribution.Data[i], + ) + } + } } func TestDecodeBuildTaggedTBTCSignerFinalizeSignRoundResponse(t *testing.T) { diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go index d9da1472fc..8c123592e3 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -51,6 +51,7 @@ type NativeTBTCSignerRoundState struct { RoundID string `json:"roundID"` RequiredContributions uint16 `json:"requiredContributions"` MessageDigestHex string `json:"messageDigestHex"` + OwnContribution *NativeTBTCSignerRoundContribution } // NativeTBTCSignerEngine executes coarse, session-keyed tbtc-signer @@ -63,6 +64,7 @@ type NativeTBTCSignerEngine interface { ) (*NativeTBTCSignerDKGResult, error) StartSignRound( sessionID string, + memberIdentifier uint16, message []byte, keyGroup string, ) (*NativeTBTCSignerRoundState, error) diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go index 088cf62f9b..4c4af005d9 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go @@ -19,9 +19,11 @@ func (mntse *mockNativeTBTCSignerEngine) RunDKG( func (mntse *mockNativeTBTCSignerEngine) StartSignRound( sessionID string, + memberIdentifier uint16, message []byte, keyGroup string, ) (*NativeTBTCSignerRoundState, error) { + _ = memberIdentifier return nil, fmt.Errorf("not implemented") } From cfc38dc84a6b129f786d84be7ee87c671c98fe3f Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 18:41:05 -0600 Subject: [PATCH 57/96] Pass signing participants into tbtc-signer start round --- ...ffi_primitive_transitional_frost_native.go | 56 ++++++++++++++++++ ...rimitive_transitional_frost_native_test.go | 55 ++++++++++++----- ...e_tbtc_signer_registration_frost_native.go | 59 ++++++++++++++++--- ...c_signer_registration_frost_native_test.go | 34 ++++++++++- .../native_tbtc_signer_engine_frost_native.go | 2 + ...ve_tbtc_signer_engine_frost_native_test.go | 2 + 6 files changed, 185 insertions(+), 23 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 1787ddb16e..5ddb255fe5 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -486,11 +486,19 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( return fmt.Errorf("request member index is zero") } + signingParticipants, err := buildTaggedTBTCSignerSigningParticipants( + includedMembersIndexes, + ) + if err != nil { + return fmt.Errorf("cannot derive signing participants: [%w]", err) + } + roundState, err := nativeEngine.StartSignRound( request.SessionID, uint16(request.MemberIndex), messageBytes, keyGroup, + signingParticipants, ) if err != nil { return fmt.Errorf("start sign round failed: [%w]", err) @@ -504,6 +512,27 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( return fmt.Errorf("start sign round required contributions are zero") } + if len(roundState.SigningParticipants) > 0 { + if len(roundState.SigningParticipants) != len(signingParticipants) { + return fmt.Errorf( + "start sign round returned unexpected signing participants count: [%v] != [%v]", + len(roundState.SigningParticipants), + len(signingParticipants), + ) + } + + for i := range signingParticipants { + if roundState.SigningParticipants[i] != signingParticipants[i] { + return fmt.Errorf( + "start sign round returned unexpected signing participant at index [%d]: [%v] != [%v]", + i, + roundState.SigningParticipants[i], + signingParticipants[i], + ) + } + } + } + roundContributions, err := buildTaggedTBTCSignerRoundContributions( ctx, request, @@ -538,6 +567,33 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( return nil } +func buildTaggedTBTCSignerSigningParticipants( + includedMembersIndexes []group.MemberIndex, +) ([]uint16, error) { + if len(includedMembersIndexes) == 0 { + return nil, fmt.Errorf("included members are empty") + } + + signingParticipants := make([]uint16, 0, len(includedMembersIndexes)) + seenParticipants := make(map[uint16]struct{}, len(includedMembersIndexes)) + + for _, memberIndex := range includedMembersIndexes { + if memberIndex == 0 { + return nil, fmt.Errorf("included member index is zero") + } + + participant := uint16(memberIndex) + if _, ok := seenParticipants[participant]; ok { + return nil, fmt.Errorf("duplicate included member index: [%v]", memberIndex) + } + + seenParticipants[participant] = struct{}{} + signingParticipants = append(signingParticipants, participant) + } + + return signingParticipants, nil +} + func buildTaggedTBTCSignerRoundContributions( ctx context.Context, request *NativeExecutionFFISigningRequest, diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 71158bd0c6..73d372ce32 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -32,20 +32,21 @@ type mockBuildTaggedTBTCSignerEngine struct { participants []NativeTBTCSignerDKGParticipant, threshold uint16, ) (*NativeTBTCSignerDKGResult, error) - version string - versionErr error - startCalled bool - startSessionID string - startMemberID uint16 - startMessage []byte - startKeyGroup string - startRoundState *NativeTBTCSignerRoundState - startErr error - finalizeCalled bool - finalizeSessionID string - finalizeInputs []NativeTBTCSignerRoundContribution - finalizeSignature []byte - finalizeErr error + version string + versionErr error + startCalled bool + startSessionID string + startMemberID uint16 + startMessage []byte + startKeyGroup string + startSigningParticipants []uint16 + startRoundState *NativeTBTCSignerRoundState + startErr error + finalizeCalled bool + finalizeSessionID string + finalizeInputs []NativeTBTCSignerRoundContribution + finalizeSignature []byte + finalizeErr error } func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( @@ -95,12 +96,14 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( memberIdentifier uint16, message []byte, keyGroup string, + signingParticipants []uint16, ) (*NativeTBTCSignerRoundState, error) { mbttse.startCalled = true mbttse.startSessionID = sessionID mbttse.startMemberID = memberIdentifier mbttse.startMessage = append([]byte{}, message...) mbttse.startKeyGroup = keyGroup + mbttse.startSigningParticipants = append([]uint16{}, signingParticipants...) if mbttse.startErr != nil { return nil, mbttse.startErr @@ -166,6 +169,7 @@ func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) StartSig memberIdentifier uint16, _ []byte, _ string, + signingParticipants []uint16, ) (*NativeTBTCSignerRoundState, error) { if dbttsbre.roundState != nil { if dbttsbre.roundState.OwnContribution == nil { @@ -178,11 +182,16 @@ func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) StartSig return dbttsbre.roundState, nil } + if len(signingParticipants) == 0 { + signingParticipants = []uint16{memberIdentifier} + } + return &NativeTBTCSignerRoundState{ SessionID: sessionID, RoundID: "round-1", RequiredContributions: 2, MessageDigestHex: "00", + SigningParticipants: append([]uint16{}, signingParticipants...), OwnContribution: &NativeTBTCSignerRoundContribution{ Identifier: memberIdentifier, Data: []byte{byte(memberIdentifier), 0xab}, @@ -1184,6 +1193,15 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } + expectedSigningParticipants := []uint16{1, 2, 3} + if !reflect.DeepEqual(engine.startSigningParticipants, expectedSigningParticipants) { + t.Fatalf( + "unexpected StartSignRound signing participants\nexpected: [%v]\nactual: [%v]", + expectedSigningParticipants, + engine.startSigningParticipants, + ) + } + if !engine.finalizeCalled { t.Fatal("expected FinalizeSignRound call in bootstrap tbtc-signer path") } @@ -1282,6 +1300,15 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } + expectedSigningParticipants := []uint16{1, 2, 3} + if !reflect.DeepEqual(engine.startSigningParticipants, expectedSigningParticipants) { + t.Fatalf( + "unexpected StartSignRound signing participants\nexpected: [%v]\nactual: [%v]", + expectedSigningParticipants, + engine.startSigningParticipants, + ) + } + if !engine.finalizeCalled { t.Fatal("expected FinalizeSignRound call in bootstrap path") } diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index e8497cff2d..05230ebc8e 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -137,10 +137,11 @@ type buildTaggedTBTCSignerRunDKGResponse struct { } type buildTaggedTBTCSignerStartSignRoundRequest struct { - SessionID string `json:"session_id"` - MemberIdentifier uint16 `json:"member_identifier"` - MessageHex string `json:"message_hex"` - KeyGroup string `json:"key_group"` + SessionID string `json:"session_id"` + MemberIdentifier uint16 `json:"member_identifier"` + MessageHex string `json:"message_hex"` + KeyGroup string `json:"key_group"` + SigningParticipants []uint16 `json:"signing_participants,omitempty"` } type buildTaggedTBTCSignerStartSignRoundResponse struct { @@ -148,6 +149,7 @@ type buildTaggedTBTCSignerStartSignRoundResponse struct { RoundID string `json:"round_id"` RequiredContributions uint16 `json:"required_contributions"` MessageDigestHex string `json:"message_digest_hex"` + SigningParticipants []uint16 `json:"signing_participants,omitempty"` OwnContribution *buildTaggedTBTCSignerFinalizeRoundContribution `json:"own_contribution"` } @@ -217,12 +219,14 @@ func (bttse *buildTaggedTBTCSignerEngine) StartSignRound( memberIdentifier uint16, message []byte, keyGroup string, + signingParticipants []uint16, ) (*NativeTBTCSignerRoundState, error) { requestPayload, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( sessionID, memberIdentifier, message, keyGroup, + signingParticipants, ) if err != nil { return nil, err @@ -402,6 +406,7 @@ func buildTaggedTBTCSignerStartSignRoundRequestPayload( memberIdentifier uint16, message []byte, keyGroup string, + signingParticipants []uint16, ) ([]byte, error) { if sessionID == "" { return nil, buildTaggedTBTCSignerOperationError( @@ -424,11 +429,29 @@ func buildTaggedTBTCSignerStartSignRoundRequestPayload( ) } + seenParticipants := make(map[uint16]struct{}, len(signingParticipants)) + for i, participant := range signingParticipants { + if participant == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + fmt.Sprintf("signing participant [%d] is zero", i), + ) + } + if _, ok := seenParticipants[participant]; ok { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + fmt.Sprintf("signing participant [%d] is duplicated", participant), + ) + } + seenParticipants[participant] = struct{}{} + } + request := buildTaggedTBTCSignerStartSignRoundRequest{ - SessionID: sessionID, - MemberIdentifier: memberIdentifier, - MessageHex: hex.EncodeToString(message), - KeyGroup: keyGroup, + SessionID: sessionID, + MemberIdentifier: memberIdentifier, + MessageHex: hex.EncodeToString(message), + KeyGroup: keyGroup, + SigningParticipants: append([]uint16{}, signingParticipants...), } payload, err := json.Marshal(request) @@ -474,6 +497,25 @@ func decodeBuildTaggedTBTCSignerStartSignRoundResponse( ) } + seenSigningParticipants := make(map[uint16]struct{}, len(response.SigningParticipants)) + for _, participant := range response.SigningParticipants { + if participant == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "response signing participant is zero", + ) + } + + if _, ok := seenSigningParticipants[participant]; ok { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + fmt.Sprintf("response signing participant [%d] is duplicated", participant), + ) + } + + seenSigningParticipants[participant] = struct{}{} + } + var ownContribution *NativeTBTCSignerRoundContribution if response.OwnContribution != nil { if response.OwnContribution.Identifier == 0 { @@ -514,6 +556,7 @@ func decodeBuildTaggedTBTCSignerStartSignRoundResponse( RoundID: response.RoundID, RequiredContributions: response.RequiredContributions, MessageDigestHex: response.MessageDigestHex, + SigningParticipants: append([]uint16{}, response.SigningParticipants...), OwnContribution: ownContribution, }, nil } diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index f3f9fb1497..e5d81cabdd 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -30,6 +30,7 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { 1, []byte("message"), "key-group", + nil, ) if err == nil { t.Fatal("expected unavailable tbtc-signer bridge error") @@ -257,6 +258,7 @@ func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload(t *testing.T) { 3, []byte{0xab, 0xcd}, "key-group-1", + []uint16{1, 2, 3}, ) if err != nil { t.Fatalf("unexpected payload build error: [%v]", err) @@ -298,6 +300,26 @@ func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload(t *testing.T) { request.MemberIdentifier, ) } + + if len(request.SigningParticipants) != 3 { + t.Fatalf( + "unexpected signing participants count\nexpected: [%v]\nactual: [%v]", + 3, + len(request.SigningParticipants), + ) + } + + expectedSigningParticipants := []uint16{1, 2, 3} + for i := range expectedSigningParticipants { + if request.SigningParticipants[i] != expectedSigningParticipants[i] { + t.Fatalf( + "unexpected signing participant at index [%d]\nexpected: [%v]\nactual: [%v]", + i, + expectedSigningParticipants[i], + request.SigningParticipants[i], + ) + } + } } func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_EmptySessionID(t *testing.T) { @@ -306,6 +328,7 @@ func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_EmptySessionID(t *tes 1, []byte{0xab}, "key-group-1", + nil, ) if !errors.Is(err, ErrNativeCryptographyUnavailable) { t.Fatalf( @@ -322,6 +345,7 @@ func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_ZeroMemberID(t *testi 0, []byte{0xab}, "key-group-1", + nil, ) if !errors.Is(err, ErrNativeCryptographyUnavailable) { t.Fatalf( @@ -387,7 +411,7 @@ func TestBuildTaggedTBTCSignerFinalizeSignRoundRequestPayload(t *testing.T) { func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse(t *testing.T) { roundState, err := decodeBuildTaggedTBTCSignerStartSignRoundResponse( []byte( - `{"session_id":"session-1","round_id":"round-1","required_contributions":2,"message_digest_hex":"abcd","own_contribution":{"identifier":3,"signature_share_hex":"deadbeef"}}`, + `{"session_id":"session-1","round_id":"round-1","required_contributions":2,"message_digest_hex":"abcd","signing_participants":[1,2,3],"own_contribution":{"identifier":3,"signature_share_hex":"deadbeef"}}`, ), ) if err != nil { @@ -426,6 +450,14 @@ func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse(t *testing.T) { ) } + if len(roundState.SigningParticipants) != 3 { + t.Fatalf( + "unexpected signing participants count\nexpected: [%v]\nactual: [%v]", + 3, + len(roundState.SigningParticipants), + ) + } + if roundState.OwnContribution == nil { t.Fatal("expected own contribution in round state response") } diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go index 8c123592e3..a0e96d805d 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -51,6 +51,7 @@ type NativeTBTCSignerRoundState struct { RoundID string `json:"roundID"` RequiredContributions uint16 `json:"requiredContributions"` MessageDigestHex string `json:"messageDigestHex"` + SigningParticipants []uint16 OwnContribution *NativeTBTCSignerRoundContribution } @@ -67,6 +68,7 @@ type NativeTBTCSignerEngine interface { memberIdentifier uint16, message []byte, keyGroup string, + signingParticipants []uint16, ) (*NativeTBTCSignerRoundState, error) FinalizeSignRound( sessionID string, diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go index 4c4af005d9..efc3f3660a 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go @@ -22,8 +22,10 @@ func (mntse *mockNativeTBTCSignerEngine) StartSignRound( memberIdentifier uint16, message []byte, keyGroup string, + signingParticipants []uint16, ) (*NativeTBTCSignerRoundState, error) { _ = memberIdentifier + _ = signingParticipants return nil, fmt.Errorf("not implemented") } From 9f555e65a6b4fbe9e84073d9661936d04b579a0b Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 19:07:59 -0600 Subject: [PATCH 58/96] Add threshold-cohort coverage for tbtc-signer bootstrap round --- ...rimitive_transitional_frost_native_test.go | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 73d372ce32..4576097dea 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -900,6 +900,191 @@ func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_ExchangesContributions } } +func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_UsesThresholdCohortOverFullGroup( + t *testing.T, +) { + provider := local.Connect() + channel, err := provider.BroadcastChannelFor("tbtc-signer-bootstrap-round-threshold-cohort-test") + if err != nil { + t.Fatalf("failed creating broadcast channel: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + primitive.RegisterUnmarshallers(channel) + + engineByMember := map[group.MemberIndex]*mockBuildTaggedTBTCSignerEngine{ + 1: { + startRoundState: &NativeTBTCSignerRoundState{ + SessionID: "session-threshold", + RoundID: "round-threshold", + RequiredContributions: 2, + MessageDigestHex: "0011", + SigningParticipants: []uint16{1, 3}, + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: 1, + Data: []byte{0x11, 0x01}, + }, + }, + }, + 3: { + startRoundState: &NativeTBTCSignerRoundState{ + SessionID: "session-threshold", + RoundID: "round-threshold", + RequiredContributions: 2, + MessageDigestHex: "0011", + SigningParticipants: []uint16{1, 3}, + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: 3, + Data: []byte{0x33, 0x03}, + }, + }, + }, + } + + requestByMember := map[group.MemberIndex]*NativeExecutionFFISigningRequest{ + 1: { + Message: big.NewInt(123), + SessionID: "session-threshold", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + Channel: channel, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 3}, + }, + }, + 3: { + Message: big.NewInt(123), + SessionID: "session-threshold", + MemberIndex: 3, + GroupSize: 3, + DishonestThreshold: 1, + Channel: channel, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 3}, + }, + }, + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelCtx() + + var wg sync.WaitGroup + signingErrors := make(chan error, len(requestByMember)) + + for memberIndex, request := range requestByMember { + engine := engineByMember[memberIndex] + wg.Add(1) + + go func( + signingRequest *NativeExecutionFFISigningRequest, + signingEngine NativeTBTCSignerEngine, + ) { + defer wg.Done() + + signingErrors <- executeBuildTaggedTBTCSignerBootstrapCoarseRound( + ctx, + signingRequest, + "group-1", + signingEngine, + ) + }(request, engine) + } + + wg.Wait() + close(signingErrors) + + for signingErr := range signingErrors { + if signingErr != nil { + t.Fatalf("unexpected signing error: [%v]", signingErr) + } + } + + expectedSigningParticipants := []uint16{1, 3} + for memberIndex, engine := range engineByMember { + if !reflect.DeepEqual(engine.startSigningParticipants, expectedSigningParticipants) { + t.Fatalf( + "unexpected StartSignRound signing participants for member [%v]\nexpected: [%v]\nactual: [%v]", + memberIndex, + expectedSigningParticipants, + engine.startSigningParticipants, + ) + } + + if len(engine.finalizeInputs) != 2 { + t.Fatalf( + "unexpected finalize input count for member [%v]\nexpected: [%v]\nactual: [%v]", + memberIndex, + 2, + len(engine.finalizeInputs), + ) + } + + if engine.finalizeInputs[0].Identifier != 1 || engine.finalizeInputs[1].Identifier != 3 { + t.Fatalf( + "unexpected finalize identifiers for member [%v]\nexpected: [1 3]\nactual: [%v %v]", + memberIndex, + engine.finalizeInputs[0].Identifier, + engine.finalizeInputs[1].Identifier, + ) + } + } +} + +func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_FailsWhenRoundStateSigningParticipantsMismatch( + t *testing.T, +) { + request := &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 2, + DishonestThreshold: 1, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + } + + engine := &mockBuildTaggedTBTCSignerEngine{ + startRoundState: &NativeTBTCSignerRoundState{ + SessionID: "session-1", + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "0011", + SigningParticipants: []uint16{1, 3}, + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: 1, + Data: []byte{0x11, 0x01}, + }, + }, + } + + err := executeBuildTaggedTBTCSignerBootstrapCoarseRound( + context.Background(), + request, + "group-1", + engine, + ) + if err == nil { + t.Fatal("expected error") + } + + expectedErrFragment := "start sign round returned unexpected signing participant" + if !strings.Contains(err.Error(), expectedErrFragment) { + t.Fatalf( + "unexpected error\nexpected to contain: [%v]\nactual: [%v]", + expectedErrFragment, + err, + ) + } +} + func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { testCases := []struct { name string From 69e844216ef6583919657655304f417f3c55b4b8 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 19:13:45 -0600 Subject: [PATCH 59/96] Add bootstrap attempt-variation cohort conflict coverage --- ...rimitive_transitional_frost_native_test.go | 201 +++++++++++++++++- 1 file changed, 194 insertions(+), 7 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 4576097dea..c15ce8a3d8 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -40,13 +40,20 @@ type mockBuildTaggedTBTCSignerEngine struct { startMessage []byte startKeyGroup string startSigningParticipants []uint16 - startRoundState *NativeTBTCSignerRoundState - startErr error - finalizeCalled bool - finalizeSessionID string - finalizeInputs []NativeTBTCSignerRoundContribution - finalizeSignature []byte - finalizeErr error + startSignRoundFn func( + sessionID string, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, + ) (*NativeTBTCSignerRoundState, error) + startRoundState *NativeTBTCSignerRoundState + startErr error + finalizeCalled bool + finalizeSessionID string + finalizeInputs []NativeTBTCSignerRoundContribution + finalizeSignature []byte + finalizeErr error } func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( @@ -109,6 +116,16 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( return nil, mbttse.startErr } + if mbttse.startSignRoundFn != nil { + return mbttse.startSignRoundFn( + sessionID, + memberIdentifier, + message, + keyGroup, + signingParticipants, + ) + } + if mbttse.startRoundState != nil { return mbttse.startRoundState, nil } @@ -1744,3 +1761,173 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } } + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_AttemptVariationStartSignRoundConflictFallsBack( + t *testing.T, +) { + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + var firstSigningParticipants []uint16 + var observedSigningParticipants [][]uint16 + + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + runDKGFn: func( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + ) (*NativeTBTCSignerDKGResult, error) { + return &NativeTBTCSignerDKGResult{ + SessionID: sessionID, + KeyGroup: "group-1", + ParticipantCount: uint16(len(participants)), + Threshold: threshold, + CreatedAtUnix: 1, + }, nil + }, + startSignRoundFn: func( + sessionID string, + _ uint16, + _ []byte, + _ string, + signingParticipants []uint16, + ) (*NativeTBTCSignerRoundState, error) { + observedSigningParticipants = append( + observedSigningParticipants, + append([]uint16{}, signingParticipants...), + ) + + if firstSigningParticipants == nil { + firstSigningParticipants = append( + []uint16{}, + signingParticipants..., + ) + } else if !reflect.DeepEqual(signingParticipants, firstSigningParticipants) { + return nil, errors.New("session_conflict") + } + + return &NativeTBTCSignerRoundState{ + SessionID: sessionID, + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "00", + SigningParticipants: append( + []uint16{}, + signingParticipants..., + ), + }, nil + }, + } + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + var observedEvents []NativeTBTCSignerFallbackEvent + err = RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + observedEvents = append(observedEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected observer registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + baseRequest := &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":"group-1","keyGroupSource":"legacy-wallet-pubkey"}`), + }, + } + + _, err = primitive.Sign(nil, nil, baseRequest) + if err == nil { + t.Fatal("expected first signing error due to legacy fallback without private key share") + } + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected first signing error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + secondRequest := *baseRequest + secondRequest.Attempt = &Attempt{ + ExcludedMembersIndexes: []group.MemberIndex{2}, + } + + _, err = primitive.Sign(nil, nil, &secondRequest) + if err == nil { + t.Fatal("expected second signing error due to legacy fallback without private key share") + } + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected second signing error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if len(observedSigningParticipants) != 2 { + t.Fatalf( + "unexpected StartSignRound call count\nexpected: [%d]\nactual: [%d]", + 2, + len(observedSigningParticipants), + ) + } + + expectedFirstParticipants := []uint16{1, 2, 3} + if !reflect.DeepEqual(observedSigningParticipants[0], expectedFirstParticipants) { + t.Fatalf( + "unexpected first StartSignRound signing participants\nexpected: [%v]\nactual: [%v]", + expectedFirstParticipants, + observedSigningParticipants[0], + ) + } + + expectedSecondParticipants := []uint16{1, 3} + if !reflect.DeepEqual(observedSigningParticipants[1], expectedSecondParticipants) { + t.Fatalf( + "unexpected second StartSignRound signing participants\nexpected: [%v]\nactual: [%v]", + expectedSecondParticipants, + observedSigningParticipants[1], + ) + } + + if len(observedEvents) != 2 { + t.Fatalf( + "unexpected fallback event count\nexpected: [%d]\nactual: [%d]", + 2, + len(observedEvents), + ) + } + + if !strings.Contains( + observedEvents[0].Reason, + "tbtc-signer bootstrap coarse round completed", + ) { + t.Fatalf( + "expected first fallback reason to include bootstrap completion\nactual: [%s]", + observedEvents[0].Reason, + ) + } + + if !strings.Contains(observedEvents[1].Reason, "session_conflict") { + t.Fatalf( + "expected second fallback reason to include session_conflict\nactual: [%s]", + observedEvents[1].Reason, + ) + } +} From 9ff8804220bf029fdab39d8add9a79ad6ff4aa3a Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 19:32:24 -0600 Subject: [PATCH 60/96] Add native FROST cohort attempt-variation coverage --- ...native_frost_protocol_frost_native_test.go | 290 +++++++++++++++++- 1 file changed, 289 insertions(+), 1 deletion(-) diff --git a/pkg/frost/signing/native_frost_protocol_frost_native_test.go b/pkg/frost/signing/native_frost_protocol_frost_native_test.go index f9c5a3e6d6..48c0ecab54 100644 --- a/pkg/frost/signing/native_frost_protocol_frost_native_test.go +++ b/pkg/frost/signing/native_frost_protocol_frost_native_test.go @@ -11,6 +11,7 @@ import ( "fmt" "math/big" "sort" + "strings" "sync" "testing" "time" @@ -149,6 +150,93 @@ func (dnfse *deterministicNativeFROSTSigningEngine) Aggregate( return signatureDigest[:], nil } +type recordingNativeFROSTSigningEngine struct { + deterministicNativeFROSTSigningEngine + mutex sync.Mutex + commitmentIDSnapshots [][]string + signatureShareIDSnapshots [][]string +} + +func (rnfse *recordingNativeFROSTSigningEngine) NewSigningPackage( + message []byte, + commitments []*NativeFROSTCommitment, +) (*NativeFROSTSigningPackage, error) { + commitmentIDs := make([]string, 0, len(commitments)) + for _, commitment := range commitments { + if commitment == nil { + commitmentIDs = append(commitmentIDs, "") + continue + } + + commitmentIDs = append(commitmentIDs, commitment.Identifier) + } + + rnfse.mutex.Lock() + rnfse.commitmentIDSnapshots = append( + rnfse.commitmentIDSnapshots, + append([]string{}, commitmentIDs...), + ) + rnfse.mutex.Unlock() + + return rnfse.deterministicNativeFROSTSigningEngine.NewSigningPackage( + message, + commitments, + ) +} + +func (rnfse *recordingNativeFROSTSigningEngine) Aggregate( + signingPackage *NativeFROSTSigningPackage, + signatureShares []*NativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, +) ([]byte, error) { + signatureShareIDs := make([]string, 0, len(signatureShares)) + for _, signatureShare := range signatureShares { + if signatureShare == nil { + signatureShareIDs = append(signatureShareIDs, "") + continue + } + + signatureShareIDs = append(signatureShareIDs, signatureShare.Identifier) + } + + rnfse.mutex.Lock() + rnfse.signatureShareIDSnapshots = append( + rnfse.signatureShareIDSnapshots, + append([]string{}, signatureShareIDs...), + ) + rnfse.mutex.Unlock() + + return rnfse.deterministicNativeFROSTSigningEngine.Aggregate( + signingPackage, + signatureShares, + publicKeyPackage, + ) +} + +func (rnfse *recordingNativeFROSTSigningEngine) commitmentIDs() [][]string { + rnfse.mutex.Lock() + defer rnfse.mutex.Unlock() + + snapshots := make([][]string, 0, len(rnfse.commitmentIDSnapshots)) + for _, snapshot := range rnfse.commitmentIDSnapshots { + snapshots = append(snapshots, append([]string{}, snapshot...)) + } + + return snapshots +} + +func (rnfse *recordingNativeFROSTSigningEngine) signatureShareIDs() [][]string { + rnfse.mutex.Lock() + defer rnfse.mutex.Unlock() + + snapshots := make([][]string, 0, len(rnfse.signatureShareIDSnapshots)) + for _, snapshot := range rnfse.signatureShareIDSnapshots { + snapshots = append(snapshots, append([]string{}, snapshot...)) + } + + return snapshots +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_NativeFROSTPath( t *testing.T, ) { @@ -231,6 +319,190 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_Nati } } +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_NativeFROSTPath_AttemptVariationUsesCohortSelections( + t *testing.T, +) { + engine := &recordingNativeFROSTSigningEngine{} + RegisterNativeFROSTSigningEngine(engine) + t.Cleanup(UnregisterNativeFROSTSigningEngine) + + provider := local.Connect() + channel, err := provider.BroadcastChannelFor("native-frost-signing-protocol-attempt-variation-test") + if err != nil { + t.Fatalf("failed creating broadcast channel: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + primitive.RegisterUnmarshallers(channel) + + runRound := func( + sessionID string, + includedMembers []group.MemberIndex, + groupSize int, + ) []*frost.Signature { + requests := make([]*NativeExecutionFFISigningRequest, len(includedMembers)) + for i := 0; i < len(includedMembers); i++ { + memberIndex := includedMembers[i] + + request, roundErr := newNativeFROSTSigningRequestWithSessionForTest( + memberIndex, + includedMembers, + channel, + groupSize, + sessionID, + ) + if roundErr != nil { + t.Fatalf( + "failed preparing request for member [%v] in session [%s]: [%v]", + memberIndex, + sessionID, + roundErr, + ) + } + + requests[i] = request + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelCtx() + + results := make([]*frostSignatureResultForTest, len(includedMembers)) + var wg sync.WaitGroup + wg.Add(len(includedMembers)) + + for i := 0; i < len(includedMembers); i++ { + go func(index int) { + defer wg.Done() + + signature, signErr := primitive.Sign(ctx, nil, requests[index]) + results[index] = &frostSignatureResultForTest{ + signature: signature, + err: signErr, + } + }(i) + } + + wg.Wait() + + signatures := make([]*frost.Signature, len(includedMembers)) + for i := 0; i < len(includedMembers); i++ { + if results[i] == nil { + t.Fatalf( + "missing signing result for member [%v] in session [%s]", + includedMembers[i], + sessionID, + ) + } + + if results[i].err != nil { + t.Fatalf( + "unexpected signing error for member [%v] in session [%s]: [%v]", + includedMembers[i], + sessionID, + results[i].err, + ) + } + + if results[i].signature == nil { + t.Fatalf( + "nil signature for member [%v] in session [%s]", + includedMembers[i], + sessionID, + ) + } + + signatures[i] = results[i].signature + } + + return signatures + } + + assertSignaturesMatch := func( + sessionID string, + signatures []*frost.Signature, + ) { + if len(signatures) == 0 { + t.Fatalf("no signatures for session [%s]", sessionID) + } + + for i := 1; i < len(signatures); i++ { + if !signatures[0].Equals(signatures[i]) { + t.Fatalf( + "signature mismatch in session [%s]\nfirst: [%v]\nsecond: [%v]", + sessionID, + signatures[0], + signatures[i], + ) + } + } + } + + roundOneSignatures := runRound( + "native-frost-signing-session-attempt-1", + []group.MemberIndex{1, 2, 3}, + 3, + ) + assertSignaturesMatch("native-frost-signing-session-attempt-1", roundOneSignatures) + + roundTwoSignatures := runRound( + "native-frost-signing-session-attempt-2", + []group.MemberIndex{1, 3}, + 3, + ) + assertSignaturesMatch("native-frost-signing-session-attempt-2", roundTwoSignatures) + + snapshotHistogram := func(snapshots [][]string) map[string]int { + histogram := make(map[string]int) + for _, snapshot := range snapshots { + histogram[strings.Join(snapshot, ",")]++ + } + + return histogram + } + + expectedHistogram := map[string]int{ + "member-1,member-2,member-3": 3, + "member-1,member-3": 2, + } + + assertHistogram := func(name string, actual map[string]int) { + if len(actual) != len(expectedHistogram) { + t.Fatalf( + "unexpected %s histogram size\nexpected: [%v]\nactual: [%v]", + name, + len(expectedHistogram), + len(actual), + ) + } + + for key, expectedCount := range expectedHistogram { + actualCount, ok := actual[key] + if !ok { + t.Fatalf("missing %s histogram key: [%s]", name, key) + } + + if actualCount != expectedCount { + t.Fatalf( + "unexpected %s count for key [%s]\nexpected: [%v]\nactual: [%v]", + name, + key, + expectedCount, + actualCount, + ) + } + } + } + + assertHistogram( + "commitment IDs", + snapshotHistogram(engine.commitmentIDs()), + ) + assertHistogram( + "signature share IDs", + snapshotHistogram(engine.signatureShareIDs()), + ) +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_NativeFROSTPathWithoutEngine( t *testing.T, ) { @@ -280,6 +552,22 @@ func newNativeFROSTSigningRequestForTest( includedMembers []group.MemberIndex, channel net.BroadcastChannel, groupSize int, +) (*NativeExecutionFFISigningRequest, error) { + return newNativeFROSTSigningRequestWithSessionForTest( + memberIndex, + includedMembers, + channel, + groupSize, + "native-frost-signing-session", + ) +} + +func newNativeFROSTSigningRequestWithSessionForTest( + memberIndex group.MemberIndex, + includedMembers []group.MemberIndex, + channel net.BroadcastChannel, + groupSize int, + sessionID string, ) (*NativeExecutionFFISigningRequest, error) { keyPackage := &NativeFROSTKeyPackage{ Identifier: fmt.Sprintf("member-%v", memberIndex), @@ -307,7 +595,7 @@ func newNativeFROSTSigningRequestForTest( return &NativeExecutionFFISigningRequest{ Message: bigOneForTest(), - SessionID: "native-frost-signing-session", + SessionID: sessionID, MemberIndex: memberIndex, GroupSize: groupSize, DishonestThreshold: 1, From d63d08bdd58851a17722ac5406bd87da3720b33b Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 19:39:10 -0600 Subject: [PATCH 61/96] Add signer-executor cohort retry integration coverage --- ...igning_native_backend_frost_native_test.go | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index b267d4b206..088647485e 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -10,7 +10,10 @@ import ( "errors" "fmt" "math/big" + "reflect" "strconv" + "strings" + "sync" "sync/atomic" "testing" @@ -18,6 +21,7 @@ import ( "github.com/keep-network/keep-core/pkg/frost" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" ) type countingNativeExecutionFFISigningPrimitive struct { @@ -28,6 +32,17 @@ type deterministicNativeExecutionFFISigningPrimitiveForTBTC struct { signCalls atomic.Int64 } +type attemptTrackingNativeExecutionFFISigningPrimitiveForTBTC struct { + signCalls atomic.Int64 + mutex sync.Mutex + records []attemptTrackingRecordForTBTC +} + +type attemptTrackingRecordForTBTC struct { + attemptNumber uint + includedMemberIndex []group.MemberIndex +} + var deterministicNativeFROSTSignatureForTBTC = [frost.SignatureSize]byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, @@ -89,6 +104,87 @@ func (dnefspf *deterministicNativeExecutionFFISigningPrimitiveForTBTC) RegisterU ) { } +func (atnefspf *attemptTrackingNativeExecutionFFISigningPrimitiveForTBTC) Sign( + ctx context.Context, + logger log.StandardLogger, + request *frostsigning.NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + atnefspf.signCalls.Add(1) + + if request == nil { + return nil, fmt.Errorf("request is nil") + } + + if request.Attempt == nil { + return nil, fmt.Errorf("request attempt is nil") + } + + atnefspf.mutex.Lock() + atnefspf.records = append( + atnefspf.records, + attemptTrackingRecordForTBTC{ + attemptNumber: request.Attempt.Number, + includedMemberIndex: append( + []group.MemberIndex{}, + request.Attempt.IncludedMembersIndexes..., + ), + }, + ) + atnefspf.mutex.Unlock() + + // Force retry-loop progression so the next attempt is exercised. + if request.Attempt.Number == 1 { + return nil, fmt.Errorf("forced attempt failure") + } + + signature := &frost.Signature{} + if err := signature.Unmarshal(deterministicNativeFROSTSignatureForTBTC[:]); err != nil { + return nil, err + } + + return signature, nil +} + +func (atnefspf *attemptTrackingNativeExecutionFFISigningPrimitiveForTBTC) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { +} + +func (atnefspf *attemptTrackingNativeExecutionFFISigningPrimitiveForTBTC) uniqueCohortsByAttempt() map[uint][][]group.MemberIndex { + atnefspf.mutex.Lock() + defer atnefspf.mutex.Unlock() + + result := make(map[uint][][]group.MemberIndex) + seen := make(map[uint]map[string]struct{}) + + for _, record := range atnefspf.records { + if seen[record.attemptNumber] == nil { + seen[record.attemptNumber] = make(map[string]struct{}) + } + + keyParts := make([]string, 0, len(record.includedMemberIndex)) + for _, memberIndex := range record.includedMemberIndex { + keyParts = append( + keyParts, + strconv.FormatUint(uint64(memberIndex), 10), + ) + } + cohortKey := strings.Join(keyParts, ",") + + if _, ok := seen[record.attemptNumber][cohortKey]; ok { + continue + } + + seen[record.attemptNumber][cohortKey] = struct{}{} + result[record.attemptNumber] = append( + result[record.attemptNumber], + append([]group.MemberIndex{}, record.includedMemberIndex...), + ) + } + + return result +} + func TestConfigureFrostSigningBackend_FFIStrictConfigured_BuildAdapter(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() @@ -349,6 +445,118 @@ func TestSigningExecutor_Sign_NativeBackend_FallsBackWhenOnlyLegacySignerMateria } } +func TestSigningExecutor_Sign_FFIStrictBackend_AttemptVariationChangesCohortSelection( + t *testing.T, +) { + executor := setupSigningExecutor(t) + configureSignersWithNativeFROSTUniFFIV2Material(t, executor) + + primitive := &attemptTrackingNativeExecutionFFISigningPrimitiveForTBTC{} + + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.UnregisterNativeExecutionBridge() + frostsigning.UnregisterNativeExecutionFFIExecutor() + frostsigning.RegisterNativeExecutionAdapterForBuild() + err := frostsigning.RegisterNativeExecutionFFISigningPrimitive(primitive) + if err != nil { + t.Fatalf("unexpected native FFI primitive registration error: [%v]", err) + } + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) + + err = configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + if err != nil { + t.Fatalf("unexpected strict ffi backend config error: [%v]", err) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + message := big.NewInt(100) + startBlock := uint64(0) + + signature, _, endBlock, err := executor.sign(ctx, message, startBlock) + if err != nil { + t.Fatalf("unexpected strict ffi signing error: [%v]", err) + } + + signatureBytes, err := signature.Marshal() + if err != nil { + t.Fatalf("cannot marshal signature: [%v]", err) + } + + if !bytes.Equal(signatureBytes, deterministicNativeFROSTSignatureForTBTC[:]) { + t.Fatalf( + "unexpected native FROST signature\nexpected: [%x]\nactual: [%x]", + deterministicNativeFROSTSignatureForTBTC[:], + signatureBytes, + ) + } + + if primitive.signCalls.Load() == 0 { + t.Fatal("expected native FFI primitive sign call") + } + + cohortsByAttempt := primitive.uniqueCohortsByAttempt() + attemptOneCohorts, ok := cohortsByAttempt[1] + if !ok { + t.Fatal("expected observed cohort for attempt 1") + } + if len(attemptOneCohorts) != 1 { + t.Fatalf( + "unexpected unique cohort count for attempt 1\nexpected: [%d]\nactual: [%d]", + 1, + len(attemptOneCohorts), + ) + } + + attemptTwoCohorts, ok := cohortsByAttempt[2] + if !ok { + t.Fatal("expected observed cohort for attempt 2") + } + if len(attemptTwoCohorts) != 1 { + t.Fatalf( + "unexpected unique cohort count for attempt 2\nexpected: [%d]\nactual: [%d]", + 1, + len(attemptTwoCohorts), + ) + } + + attemptOneCohort := attemptOneCohorts[0] + attemptTwoCohort := attemptTwoCohorts[0] + + expectedCohortSize := executor.groupParameters.HonestThreshold + if len(attemptOneCohort) != expectedCohortSize { + t.Fatalf( + "unexpected cohort size for attempt 1\nexpected: [%d]\nactual: [%d]", + expectedCohortSize, + len(attemptOneCohort), + ) + } + if len(attemptTwoCohort) != expectedCohortSize { + t.Fatalf( + "unexpected cohort size for attempt 2\nexpected: [%d]\nactual: [%d]", + expectedCohortSize, + len(attemptTwoCohort), + ) + } + + if reflect.DeepEqual(attemptOneCohort, attemptTwoCohort) { + t.Fatalf( + "expected cohort variation across attempts\nattempt 1: [%v]\nattempt 2: [%v]", + attemptOneCohort, + attemptTwoCohort, + ) + } + + if endBlock <= startBlock { + t.Fatal("wrong end block") + } +} + func configureSignersWithNativeFROSTUniFFIV2Material( t *testing.T, executor *signingExecutor, From 7814f81a93ecd9ee5e11b587d3e2bbdd903fbb3e Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 23 Feb 2026 20:07:09 -0600 Subject: [PATCH 62/96] Add tbtc-signer runtime cohort retry integration test --- ...igning_native_backend_frost_native_test.go | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index 088647485e..0a920cf3e5 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "crypto/ecdsa" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -43,6 +44,11 @@ type attemptTrackingRecordForTBTC struct { includedMemberIndex []group.MemberIndex } +type attemptTrackingNativeTBTCSignerEngineForTBTC struct { + mutex sync.Mutex + startCohortsByAttempt map[uint][][]uint16 +} + var deterministicNativeFROSTSignatureForTBTC = [frost.SignatureSize]byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, @@ -185,6 +191,137 @@ func (atnefspf *attemptTrackingNativeExecutionFFISigningPrimitiveForTBTC) unique return result } +func (atntsfe *attemptTrackingNativeTBTCSignerEngineForTBTC) Version() (string, error) { + return "tbtc-signer/0.1.0-bootstrap", nil +} + +func (atntsfe *attemptTrackingNativeTBTCSignerEngineForTBTC) RunDKG( + sessionID string, + participants []frostsigning.NativeTBTCSignerDKGParticipant, + threshold uint16, +) (*frostsigning.NativeTBTCSignerDKGResult, error) { + return &frostsigning.NativeTBTCSignerDKGResult{ + SessionID: sessionID, + KeyGroup: "group-1", + ParticipantCount: uint16(len(participants)), + Threshold: threshold, + CreatedAtUnix: 1, + }, nil +} + +func (atntsfe *attemptTrackingNativeTBTCSignerEngineForTBTC) StartSignRound( + sessionID string, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, +) (*frostsigning.NativeTBTCSignerRoundState, error) { + attemptNumber, err := attemptNumberFromSessionIDForTBTC(sessionID) + if err != nil { + return nil, err + } + + if keyGroup == "" { + return nil, fmt.Errorf("key group is empty") + } + + if memberIdentifier == 0 { + return nil, fmt.Errorf("member identifier is zero") + } + + if len(message) == 0 { + return nil, fmt.Errorf("message is empty") + } + + if len(signingParticipants) == 0 { + return nil, fmt.Errorf("signing participants are empty") + } + + atntsfe.mutex.Lock() + if atntsfe.startCohortsByAttempt == nil { + atntsfe.startCohortsByAttempt = make(map[uint][][]uint16) + } + + cohort := append([]uint16{}, signingParticipants...) + atntsfe.startCohortsByAttempt[attemptNumber] = append( + atntsfe.startCohortsByAttempt[attemptNumber], + cohort, + ) + atntsfe.mutex.Unlock() + + return &frostsigning.NativeTBTCSignerRoundState{ + SessionID: sessionID, + RoundID: fmt.Sprintf("round-%v", attemptNumber), + RequiredContributions: uint16(len(signingParticipants)), + MessageDigestHex: "00", + SigningParticipants: append([]uint16{}, signingParticipants...), + OwnContribution: &frostsigning.NativeTBTCSignerRoundContribution{ + Identifier: memberIdentifier, + Data: []byte{byte(memberIdentifier), byte(attemptNumber)}, + }, + }, nil +} + +func (atntsfe *attemptTrackingNativeTBTCSignerEngineForTBTC) FinalizeSignRound( + sessionID string, + roundContributions []frostsigning.NativeTBTCSignerRoundContribution, +) ([]byte, error) { + if _, err := attemptNumberFromSessionIDForTBTC(sessionID); err != nil { + return nil, err + } + + if len(roundContributions) == 0 { + return nil, fmt.Errorf("round contributions are empty") + } + + return []byte{0xaa}, nil +} + +func (atntsfe *attemptTrackingNativeTBTCSignerEngineForTBTC) uniqueStartCohortsByAttempt() map[uint][][]uint16 { + atntsfe.mutex.Lock() + defer atntsfe.mutex.Unlock() + + result := make(map[uint][][]uint16) + seen := make(map[uint]map[string]struct{}) + + for attemptNumber, cohorts := range atntsfe.startCohortsByAttempt { + if seen[attemptNumber] == nil { + seen[attemptNumber] = make(map[string]struct{}) + } + + for _, cohort := range cohorts { + parts := make([]string, 0, len(cohort)) + for _, participant := range cohort { + parts = append(parts, strconv.FormatUint(uint64(participant), 10)) + } + key := strings.Join(parts, ",") + + if _, ok := seen[attemptNumber][key]; ok { + continue + } + + seen[attemptNumber][key] = struct{}{} + result[attemptNumber] = append(result[attemptNumber], append([]uint16{}, cohort...)) + } + } + + return result +} + +func attemptNumberFromSessionIDForTBTC(sessionID string) (uint, error) { + separatorIndex := strings.LastIndex(sessionID, "-") + if separatorIndex < 0 || separatorIndex == len(sessionID)-1 { + return 0, fmt.Errorf("invalid session id format: [%s]", sessionID) + } + + attemptNumber, err := strconv.ParseUint(sessionID[separatorIndex+1:], 10, 64) + if err != nil { + return 0, fmt.Errorf("cannot parse attempt number from session id [%s]: [%w]", sessionID, err) + } + + return uint(attemptNumber), nil +} + func TestConfigureFrostSigningBackend_FFIStrictConfigured_BuildAdapter(t *testing.T) { frostsigning.ResetExecutionBackend() frostsigning.UnregisterNativeExecutionAdapter() @@ -557,6 +694,152 @@ func TestSigningExecutor_Sign_FFIStrictBackend_AttemptVariationChangesCohortSele } } +func TestSigningExecutor_Sign_FFIStrictBackend_TBTCSignerPath_AttemptVariationChangesCohortSelection( + t *testing.T, +) { + executor := setupSigningExecutor(t) + configureSignersWithTBTCSignerMaterial(t, executor, 3) + + nativeTBTCSignerEngine := &attemptTrackingNativeTBTCSignerEngineForTBTC{} + + frostsigning.UnregisterNativeTBTCSignerEngine() + frostsigning.UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(frostsigning.UnregisterNativeTBTCSignerEngine) + t.Cleanup(frostsigning.UnregisterNativeTBTCSignerFallbackObserver) + + err := frostsigning.RegisterNativeTBTCSignerEngine(nativeTBTCSignerEngine) + if err != nil { + t.Fatalf("unexpected native tbtc-signer engine registration error: [%v]", err) + } + + var fallbackEvents []frostsigning.NativeTBTCSignerFallbackEvent + err = frostsigning.RegisterNativeTBTCSignerFallbackObserver( + func(event frostsigning.NativeTBTCSignerFallbackEvent) { + fallbackEvents = append(fallbackEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected fallback observer registration error: [%v]", err) + } + + frostsigning.ResetExecutionBackend() + frostsigning.UnregisterNativeExecutionAdapter() + frostsigning.UnregisterNativeExecutionBridge() + frostsigning.UnregisterNativeExecutionFFIExecutor() + frostsigning.RegisterNativeExecutionAdapterForBuild() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) + + err = configureFrostSigningBackend(Config{FrostSigningBackend: "ffi"}) + if err != nil { + t.Fatalf("unexpected strict ffi backend config error: [%v]", err) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + defer cancelCtx() + + message := big.NewInt(100) + startBlock := uint64(0) + + signature, _, endBlock, err := executor.sign(ctx, message, startBlock) + if err != nil { + t.Fatalf("unexpected strict ffi tbtc-signer-path signing error: [%v]", err) + } + + walletPublicKey := executor.wallet().publicKey + if !ecdsa.Verify( + walletPublicKey, + message.Bytes(), + new(big.Int).SetBytes(signature.R[:]), + new(big.Int).SetBytes(signature.S[:]), + ) { + t.Fatalf("invalid signature: [%+v]", signature) + } + + cohortsByAttempt := nativeTBTCSignerEngine.uniqueStartCohortsByAttempt() + attemptOneCohorts, ok := cohortsByAttempt[1] + if !ok { + t.Fatal("expected observed StartSignRound cohort for attempt 1") + } + if len(attemptOneCohorts) != 1 { + t.Fatalf( + "unexpected unique cohort count for attempt 1\nexpected: [%d]\nactual: [%d]", + 1, + len(attemptOneCohorts), + ) + } + + attemptTwoCohorts, ok := cohortsByAttempt[2] + if !ok { + t.Fatal("expected observed StartSignRound cohort for attempt 2") + } + if len(attemptTwoCohorts) != 1 { + t.Fatalf( + "unexpected unique cohort count for attempt 2\nexpected: [%d]\nactual: [%d]", + 1, + len(attemptTwoCohorts), + ) + } + + attemptOneCohort := attemptOneCohorts[0] + attemptTwoCohort := attemptTwoCohorts[0] + + expectedCohortSize := executor.groupParameters.HonestThreshold + if len(attemptOneCohort) != expectedCohortSize { + t.Fatalf( + "unexpected cohort size for attempt 1\nexpected: [%d]\nactual: [%d]", + expectedCohortSize, + len(attemptOneCohort), + ) + } + if len(attemptTwoCohort) != expectedCohortSize { + t.Fatalf( + "unexpected cohort size for attempt 2\nexpected: [%d]\nactual: [%d]", + expectedCohortSize, + len(attemptTwoCohort), + ) + } + + if !containsParticipantForTBTC(attemptOneCohort, 3) { + t.Fatalf( + "expected attempt 1 cohort to include broken signer member 3\nactual: [%v]", + attemptOneCohort, + ) + } + + if containsParticipantForTBTC(attemptTwoCohort, 3) { + t.Fatalf( + "expected attempt 2 cohort to exclude broken signer member 3\nactual: [%v]", + attemptTwoCohort, + ) + } + + if reflect.DeepEqual(attemptOneCohort, attemptTwoCohort) { + t.Fatalf( + "expected cohort variation across attempts\nattempt 1: [%v]\nattempt 2: [%v]", + attemptOneCohort, + attemptTwoCohort, + ) + } + + missingLegacyFallbackObserved := false + for _, event := range fallbackEvents { + if !event.LegacyPrivateKeyShareExists { + missingLegacyFallbackObserved = true + break + } + } + if !missingLegacyFallbackObserved { + t.Fatal("expected at least one fallback event without legacy private key share") + } + + if endBlock <= startBlock { + t.Fatal("wrong end block") + } +} + func configureSignersWithNativeFROSTUniFFIV2Material( t *testing.T, executor *signingExecutor, @@ -593,3 +876,47 @@ func configureSignersWithNativeFROSTUniFFIV2Material( } } } + +func configureSignersWithTBTCSignerMaterial( + t *testing.T, + executor *signingExecutor, + brokenMemberIndex group.MemberIndex, +) { + t.Helper() + + for _, signer := range executor.signers { + legacyPrivateKeyShareHex := "" + if signer.signingGroupMemberIndex != brokenMemberIndex { + legacyPrivateKeySharePayload, err := signer.privateKeyShare.Marshal() + if err != nil { + t.Fatalf("cannot marshal private key share: [%v]", err) + } + + legacyPrivateKeyShareHex = hex.EncodeToString(legacyPrivateKeySharePayload) + } + + payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ + KeyGroup: "group-1", + KeyGroupSource: frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + LegacyPrivateKeyShareHex: legacyPrivateKeyShareHex, + }) + if err != nil { + t.Fatalf("cannot marshal tbtc-signer material payload: [%v]", err) + } + + signer.signerMaterial = &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + } +} + +func containsParticipantForTBTC(cohort []uint16, memberIndex uint16) bool { + for _, participant := range cohort { + if participant == memberIndex { + return true + } + } + + return false +} From 7f7b9a2d972e3f9e0e22331bc5046586454bdd1e Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 07:57:06 -0600 Subject: [PATCH 63/96] Fix coarse-round participant validation and member derivation consistency --- ...ffi_primitive_transitional_frost_native.go | 46 +++++++++-- ...rimitive_transitional_frost_native_test.go | 80 +++++++++++++++++++ ...c_signer_registration_frost_native_test.go | 63 +++++++++++++++ 3 files changed, 184 insertions(+), 5 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 5ddb255fe5..a38e815988 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -172,7 +172,22 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) ) } - dkgParticipants, dkgThreshold, err := buildTaggedTBTCSignerRunDKGInputs(request) + includedMembersSet, includedMembersIndexes, err := includedMembersFromRequest(request) + if err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf("cannot determine included members: [%v]", err), + payload.KeyGroupSource, + ) + } + + dkgParticipants, dkgThreshold, err := buildTaggedTBTCSignerRunDKGInputsForIncludedMembers( + request, + includedMembersIndexes, + ) if err != nil { return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, @@ -292,6 +307,8 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) request, keyGroupForRound, nativeEngine, + includedMembersSet, + includedMembersIndexes, ); err != nil { return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, @@ -327,6 +344,20 @@ func buildTaggedTBTCSignerRunDKGInputs( return nil, 0, err } + return buildTaggedTBTCSignerRunDKGInputsForIncludedMembers( + request, + includedMembersIndexes, + ) +} + +func buildTaggedTBTCSignerRunDKGInputsForIncludedMembers( + request *NativeExecutionFFISigningRequest, + includedMembersIndexes []group.MemberIndex, +) ([]NativeTBTCSignerDKGParticipant, uint16, error) { + if request == nil { + return nil, 0, fmt.Errorf("request is nil") + } + if len(includedMembersIndexes) < 2 { return nil, 0, fmt.Errorf("insufficient included members for DKG") } @@ -448,6 +479,8 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( request *NativeExecutionFFISigningRequest, keyGroup string, nativeEngine NativeTBTCSignerEngine, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, ) error { if request == nil { return fmt.Errorf("request is nil") @@ -465,9 +498,12 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( ctx = context.Background() } - includedMembersSet, includedMembersIndexes, err := includedMembersFromRequest(request) - if err != nil { - return fmt.Errorf("cannot determine included members: [%w]", err) + if includedMembersSet == nil || len(includedMembersIndexes) == 0 { + var err error + includedMembersSet, includedMembersIndexes, err = includedMembersFromRequest(request) + if err != nil { + return fmt.Errorf("cannot determine included members: [%w]", err) + } } if _, ok := includedMembersSet[request.MemberIndex]; !ok { @@ -512,7 +548,7 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( return fmt.Errorf("start sign round required contributions are zero") } - if len(roundState.SigningParticipants) > 0 { + if len(signingParticipants) > 0 { if len(roundState.SigningParticipants) != len(signingParticipants) { return fmt.Errorf( "start sign round returned unexpected signing participants count: [%v] != [%v]", diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index c15ce8a3d8..add0a2c526 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -127,6 +127,13 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( } if mbttse.startRoundState != nil { + if len(mbttse.startRoundState.SigningParticipants) == 0 { + mbttse.startRoundState.SigningParticipants = append( + []uint16{}, + signingParticipants..., + ) + } + return mbttse.startRoundState, nil } @@ -135,6 +142,7 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( RoundID: "round-1", RequiredContributions: 2, MessageDigestHex: "00", + SigningParticipants: append([]uint16{}, signingParticipants...), }, nil } @@ -196,6 +204,13 @@ func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) StartSig } } + if len(dbttsbre.roundState.SigningParticipants) == 0 { + dbttsbre.roundState.SigningParticipants = append( + []uint16{}, + signingParticipants..., + ) + } + return dbttsbre.roundState, nil } @@ -860,6 +875,8 @@ func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_ExchangesContributions signingRequest, "group-1", signingEngine, + nil, + nil, ) }(request, engine) } @@ -1008,6 +1025,8 @@ func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_UsesThresholdCohortOve signingRequest, "group-1", signingEngine, + nil, + nil, ) }(request, engine) } @@ -1087,6 +1106,8 @@ func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_FailsWhenRoundStateSig request, "group-1", engine, + nil, + nil, ) if err == nil { t.Fatal("expected error") @@ -1102,6 +1123,65 @@ func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_FailsWhenRoundStateSig } } +func TestExecuteBuildTaggedTBTCSignerBootstrapCoarseRound_FailsWhenRoundStateSigningParticipantsMissing( + t *testing.T, +) { + request := &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 2, + DishonestThreshold: 1, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + } + + engine := &mockBuildTaggedTBTCSignerEngine{ + startSignRoundFn: func( + sessionID string, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, + ) (*NativeTBTCSignerRoundState, error) { + return &NativeTBTCSignerRoundState{ + SessionID: sessionID, + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "0011", + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: memberIdentifier, + Data: []byte{0x11, 0x01}, + }, + }, nil + }, + } + + err := executeBuildTaggedTBTCSignerBootstrapCoarseRound( + context.Background(), + request, + "group-1", + engine, + nil, + nil, + ) + if err == nil { + t.Fatal("expected error") + } + + expectedErrFragment := "start sign round returned unexpected signing participants count" + if !strings.Contains(err.Error(), expectedErrFragment) { + t.Fatalf( + "unexpected error\nexpected to contain: [%v]\nactual: [%v]", + expectedErrFragment, + err, + ) + } +} + func TestBuildTaggedTBTCSignerRoundKeyGroup(t *testing.T) { testCases := []struct { name string diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index e5d81cabdd..2b118338ed 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -491,6 +491,69 @@ func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse(t *testing.T) { } } +func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse_RejectsZeroSigningParticipant( + t *testing.T, +) { + _, err := decodeBuildTaggedTBTCSignerStartSignRoundResponse( + []byte( + `{"session_id":"session-1","round_id":"round-1","required_contributions":2,"message_digest_hex":"abcd","signing_participants":[1,0,3],"own_contribution":{"identifier":3,"signature_share_hex":"deadbeef"}}`, + ), + ) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } +} + +func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse_RejectsDuplicateSigningParticipant( + t *testing.T, +) { + _, err := decodeBuildTaggedTBTCSignerStartSignRoundResponse( + []byte( + `{"session_id":"session-1","round_id":"round-1","required_contributions":2,"message_digest_hex":"abcd","signing_participants":[1,2,2],"own_contribution":{"identifier":3,"signature_share_hex":"deadbeef"}}`, + ), + ) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } +} + +func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse_RejectsZeroOwnContributionIdentifier( + t *testing.T, +) { + _, err := decodeBuildTaggedTBTCSignerStartSignRoundResponse( + []byte( + `{"session_id":"session-1","round_id":"round-1","required_contributions":2,"message_digest_hex":"abcd","signing_participants":[1,2,3],"own_contribution":{"identifier":0,"signature_share_hex":"deadbeef"}}`, + ), + ) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } +} + func TestDecodeBuildTaggedTBTCSignerFinalizeSignRoundResponse(t *testing.T) { signature, err := decodeBuildTaggedTBTCSignerFinalizeSignRoundResponse( []byte(`{"session_id":"session-1","round_id":"round-1","signature_hex":"deadbeef"}`), From 7012a162964cfa9a82f032da1bca745b81e5c7e2 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 08:01:20 -0600 Subject: [PATCH 64/96] Run gofmt on signing request struct --- pkg/frost/signing/request.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/frost/signing/request.go b/pkg/frost/signing/request.go index 2d6eef7052..e14b4da13c 100644 --- a/pkg/frost/signing/request.go +++ b/pkg/frost/signing/request.go @@ -11,9 +11,9 @@ import ( // Request carries execution input for a FROST signing backend. type Request struct { - Message *big.Int - SessionID string - MemberIndex group.MemberIndex + Message *big.Int + SessionID string + MemberIndex group.MemberIndex // SignerMaterial carries backend-specific signer material. // Legacy backend expects *tecdsa.PrivateKeyShare. SignerMaterial any From ad2aeb5a8d1f29c0b091cc35e5c199d52c83e540 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 08:15:19 -0600 Subject: [PATCH 65/96] Fix tbtc signer material symbols for non-frost builds --- .../native_tbtc_signer_engine_frost_native.go | 17 ----------------- .../signing/native_tbtc_signer_material.go | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 pkg/frost/signing/native_tbtc_signer_material.go diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go index a0e96d805d..5fec7c4b1b 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -4,23 +4,6 @@ package signing import "fmt" -const ( - // NativeSignerMaterialFormatFrostTBTCSignerV1 carries signer material for - // tbtc-signer coarse session APIs. - NativeSignerMaterialFormatFrostTBTCSignerV1 = "frost-tbtc-signer-v1" - // NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey marks scaffold-era - // key-group derivation from the legacy wallet public key. - NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey = "legacy-wallet-pubkey" -) - -// NativeTBTCSignerMaterialPayload is the signer-material payload schema for -// `frost-tbtc-signer-v1`. -type NativeTBTCSignerMaterialPayload struct { - KeyGroup string `json:"keyGroup"` - KeyGroupSource string `json:"keyGroupSource,omitempty"` - LegacyPrivateKeyShareHex string `json:"legacyPrivateKeyShareHex,omitempty"` -} - // NativeTBTCSignerDKGParticipant identifies a DKG participant for coarse // tbtc-signer RunDKG operation. type NativeTBTCSignerDKGParticipant struct { diff --git a/pkg/frost/signing/native_tbtc_signer_material.go b/pkg/frost/signing/native_tbtc_signer_material.go new file mode 100644 index 0000000000..ad8b443ad9 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_material.go @@ -0,0 +1,18 @@ +package signing + +const ( + // NativeSignerMaterialFormatFrostTBTCSignerV1 carries signer material for + // tbtc-signer coarse session APIs. + NativeSignerMaterialFormatFrostTBTCSignerV1 = "frost-tbtc-signer-v1" + // NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey marks scaffold-era + // key-group derivation from the legacy wallet public key. + NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey = "legacy-wallet-pubkey" +) + +// NativeTBTCSignerMaterialPayload is the signer-material payload schema for +// `frost-tbtc-signer-v1`. +type NativeTBTCSignerMaterialPayload struct { + KeyGroup string `json:"keyGroup"` + KeyGroupSource string `json:"keyGroupSource,omitempty"` + LegacyPrivateKeyShareHex string `json:"legacyPrivateKeyShareHex,omitempty"` +} From a7157c97e83cf16286e10d7f90c6f3e7073700fb Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 09:40:14 -0600 Subject: [PATCH 66/96] Update tbtcpg LocalChain test double for BridgeChain method --- pkg/tbtcpg/chain_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 52f6ef4137..af56f9ffcf 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -1047,6 +1047,25 @@ func (lc *LocalChain) GetWallet(walletPublicKeyHash [20]byte) ( return data, nil } +func (lc *LocalChain) WalletPublicKeyHashForWalletID( + walletID [32]byte, +) ([20]byte, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + for walletPublicKeyHash, walletData := range lc.walletChainData { + if walletData == nil { + continue + } + + if walletData.WalletID == walletID || walletData.EcdsaWalletID == walletID { + return walletPublicKeyHash, nil + } + } + + return [20]byte{}, fmt.Errorf("wallet public key hash for wallet ID not found") +} + func (lc *LocalChain) SetWallet( walletPublicKeyHash [20]byte, data *tbtc.WalletChainData, From d4e832322ee1d95a40275100a63b0d68d65100a1 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 09:47:17 -0600 Subject: [PATCH 67/96] Make tbtc bridge access resilient to generated API variants --- pkg/chain/ethereum/tbtc.go | 184 +++++++++++++++++++++++++++++++------ 1 file changed, 154 insertions(+), 30 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index f9d0d30c17..9e256bc69d 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1392,19 +1392,11 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( walletID, ecdsaWalletID, walletPublicKeyHash, - tc.bridge.PastNewWalletRegisteredV2Events, + tc.bridge, tc.bridge.PastNewWalletRegisteredEvents, ) } -type pastNewWalletRegisteredV2EventsFn func( - startBlock uint64, - endBlock *uint64, - walletID [][32]byte, - ecdsaWalletID [][32]byte, - walletPubKeyHash [][20]byte, -) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) - type pastNewWalletRegisteredEventsFn func( startBlock uint64, endBlock *uint64, @@ -1418,32 +1410,21 @@ func pastNewWalletRegisteredEvents( walletID [][32]byte, ecdsaWalletID [][32]byte, walletPublicKeyHash [][20]byte, - pastV2Events pastNewWalletRegisteredV2EventsFn, + bridge any, pastLegacyEvents pastNewWalletRegisteredEventsFn, ) ([]*tbtc.NewWalletRegisteredEvent, error) { - v2Events, err := pastV2Events( + convertedEvents, err := pastNewWalletRegisteredV2Events( startBlock, endBlock, walletID, ecdsaWalletID, walletPublicKeyHash, + bridge, ) if err != nil { return nil, err } - convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0, len(v2Events)) - for _, event := range v2Events { - convertedEvent := &tbtc.NewWalletRegisteredEvent{ - WalletID: event.WalletID, - EcdsaWalletID: event.EcdsaWalletID, - WalletPublicKeyHash: event.WalletPubKeyHash, - BlockNumber: event.Raw.BlockNumber, - } - - convertedEvents = append(convertedEvents, convertedEvent) - } - // Fallback for legacy deployments that do not emit NewWalletRegisteredV2. if len(convertedEvents) == 0 && len(walletID) == 0 { legacyEvents, err := pastLegacyEvents( @@ -1478,6 +1459,118 @@ func pastNewWalletRegisteredEvents( return convertedEvents, nil } +func pastNewWalletRegisteredV2Events( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPublicKeyHash [][20]byte, + bridge any, +) ([]*tbtc.NewWalletRegisteredEvent, error) { + bridgeValue := reflect.ValueOf(bridge) + pastV2Events := bridgeValue.MethodByName("PastNewWalletRegisteredV2Events") + if !pastV2Events.IsValid() { + return nil, nil + } + + results := pastV2Events.Call( + []reflect.Value{ + reflect.ValueOf(startBlock), + reflect.ValueOf(endBlock), + reflect.ValueOf(walletID), + reflect.ValueOf(ecdsaWalletID), + reflect.ValueOf(walletPublicKeyHash), + }, + ) + if len(results) != 2 { + return nil, fmt.Errorf( + "unexpected PastNewWalletRegisteredV2Events result count: [%v]", + len(results), + ) + } + + if !results[1].IsNil() { + err, ok := results[1].Interface().(error) + if !ok { + return nil, fmt.Errorf( + "unexpected PastNewWalletRegisteredV2Events error type: [%T]", + results[1].Interface(), + ) + } + + return nil, err + } + + eventsValue := results[0] + if eventsValue.Kind() != reflect.Slice { + return nil, fmt.Errorf( + "unexpected PastNewWalletRegisteredV2Events events type: [%v]", + eventsValue.Kind(), + ) + } + + convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0, eventsValue.Len()) + for i := 0; i < eventsValue.Len(); i++ { + eventValue := eventsValue.Index(i) + if eventValue.Kind() == reflect.Pointer { + if eventValue.IsNil() { + continue + } + + eventValue = eventValue.Elem() + } + + if eventValue.Kind() != reflect.Struct { + return nil, fmt.Errorf( + "unexpected NewWalletRegisteredV2 event kind: [%v]", + eventValue.Kind(), + ) + } + + walletIDField := eventValue.FieldByName("WalletID") + ecdsaWalletIDField := eventValue.FieldByName("EcdsaWalletID") + walletPubKeyHashField := eventValue.FieldByName("WalletPubKeyHash") + if !walletPubKeyHashField.IsValid() { + walletPubKeyHashField = eventValue.FieldByName("WalletPublicKeyHash") + } + rawField := eventValue.FieldByName("Raw") + if rawField.Kind() == reflect.Pointer { + if rawField.IsNil() { + return nil, fmt.Errorf("unexpected nil raw event payload") + } + + rawField = rawField.Elem() + } + blockNumberField := rawField.FieldByName("BlockNumber") + + if !walletIDField.IsValid() || + walletIDField.Type() != reflect.TypeOf([32]byte{}) || + !ecdsaWalletIDField.IsValid() || + ecdsaWalletIDField.Type() != reflect.TypeOf([32]byte{}) || + !walletPubKeyHashField.IsValid() || + walletPubKeyHashField.Type() != reflect.TypeOf([20]byte{}) || + !blockNumberField.IsValid() || + blockNumberField.Kind() != reflect.Uint64 { + return nil, fmt.Errorf( + "unexpected NewWalletRegisteredV2 event shape at index [%v]", + i, + ) + } + + convertedEvents = append( + convertedEvents, + &tbtc.NewWalletRegisteredEvent{ + WalletID: walletIDField.Interface().([32]byte), + EcdsaWalletID: ecdsaWalletIDField.Interface().([32]byte), + WalletPublicKeyHash: walletPubKeyHashField.Interface().([20]byte), + BlockNumber: blockNumberField.Uint(), + }, + ) + } + + return convertedEvents, nil +} + func (tc *TbtcChain) CalculateWalletID( walletPublicKey *ecdsa.PublicKey, ) ([32]byte, error) { @@ -1536,7 +1629,10 @@ func (tc *TbtcChain) GetWallet( return nil, fmt.Errorf("cannot parse wallet state: [%v]", err) } - walletID, err := tc.bridge.WalletID(walletPublicKeyHash) + walletID, err := walletIDForWalletPublicKeyHash( + tc.bridge, + walletPublicKeyHash, + ) if err != nil { // Fallback for legacy deployments where walletID accessor may not exist. walletID = tbtc.DeriveLegacyWalletID(walletPublicKeyHash) @@ -1561,19 +1657,47 @@ func (tc *TbtcChain) WalletPublicKeyHashForWalletID( ) ([20]byte, error) { return resolveWalletPublicKeyHashForWalletID( walletID, - tc.bridge.WalletPubKeyHashForWalletID, + tc.bridge, ) } -type walletPublicKeyHashForWalletIDFn func( - walletID [32]byte, -) ([20]byte, error) +type walletIDForWalletPublicKeyHashFn interface { + WalletID(walletPublicKeyHash [20]byte) ([32]byte, error) +} + +func walletIDForWalletPublicKeyHash( + bridge any, + walletPublicKeyHash [20]byte, +) ([32]byte, error) { + resolver, ok := bridge.(walletIDForWalletPublicKeyHashFn) + if !ok { + return [32]byte{}, fmt.Errorf("wallet ID accessor unavailable") + } + + return resolver.WalletID(walletPublicKeyHash) +} + +type walletPublicKeyHashForWalletIDFn interface { + WalletPubKeyHashForWalletID(walletID [32]byte) ([20]byte, error) +} func resolveWalletPublicKeyHashForWalletID( walletID [32]byte, - resolveCanonical walletPublicKeyHashForWalletIDFn, + bridge any, ) ([20]byte, error) { - walletPublicKeyHash, err := resolveCanonical(walletID) + resolveCanonical, ok := bridge.(walletPublicKeyHashForWalletIDFn) + if !ok { + resolveCanonical = nil + } + + var walletPublicKeyHash [20]byte + var err error + if resolveCanonical != nil { + walletPublicKeyHash, err = resolveCanonical.WalletPubKeyHashForWalletID(walletID) + } else { + err = fmt.Errorf("wallet public key hash accessor unavailable") + } + if err == nil { if walletPublicKeyHash != [20]byte{} { return walletPublicKeyHash, nil From c372effdc8bd9c143bb71e45a065f18554be0f55 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 10:58:32 -0600 Subject: [PATCH 68/96] Harden V2 wallet event reflection and expand coverage --- pkg/chain/ethereum/tbtc.go | 70 +++- pkg/chain/ethereum/tbtc_test.go | 344 +++++++++++++++--- .../native_tbtc_signer_engine_frost_native.go | 12 +- 3 files changed, 363 insertions(+), 63 deletions(-) diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 9e256bc69d..33f65e63be 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -1467,21 +1467,46 @@ func pastNewWalletRegisteredV2Events( walletPublicKeyHash [][20]byte, bridge any, ) ([]*tbtc.NewWalletRegisteredEvent, error) { + if bridge == nil { + return nil, nil + } + bridgeValue := reflect.ValueOf(bridge) pastV2Events := bridgeValue.MethodByName("PastNewWalletRegisteredV2Events") if !pastV2Events.IsValid() { return nil, nil } - results := pastV2Events.Call( - []reflect.Value{ - reflect.ValueOf(startBlock), - reflect.ValueOf(endBlock), - reflect.ValueOf(walletID), - reflect.ValueOf(ecdsaWalletID), - reflect.ValueOf(walletPublicKeyHash), - }, + var ( + results []reflect.Value + callErr error ) + + func() { + defer func() { + if recovered := recover(); recovered != nil { + callErr = fmt.Errorf( + "panic calling PastNewWalletRegisteredV2Events: [%v]", + recovered, + ) + } + }() + + results = pastV2Events.Call( + []reflect.Value{ + reflect.ValueOf(startBlock), + reflect.ValueOf(endBlock), + reflect.ValueOf(walletID), + reflect.ValueOf(ecdsaWalletID), + reflect.ValueOf(walletPublicKeyHash), + }, + ) + }() + + if callErr != nil { + return nil, callErr + } + if len(results) != 2 { return nil, fmt.Errorf( "unexpected PastNewWalletRegisteredV2Events result count: [%v]", @@ -1534,6 +1559,13 @@ func pastNewWalletRegisteredV2Events( walletPubKeyHashField = eventValue.FieldByName("WalletPublicKeyHash") } rawField := eventValue.FieldByName("Raw") + if !rawField.IsValid() { + return nil, fmt.Errorf( + "unexpected NewWalletRegisteredV2 raw event payload at index [%v]", + i, + ) + } + if rawField.Kind() == reflect.Pointer { if rawField.IsNil() { return nil, fmt.Errorf("unexpected nil raw event payload") @@ -1541,6 +1573,15 @@ func pastNewWalletRegisteredV2Events( rawField = rawField.Elem() } + + if rawField.Kind() != reflect.Struct { + return nil, fmt.Errorf( + "unexpected NewWalletRegisteredV2 raw event payload kind at index [%v]: [%v]", + i, + rawField.Kind(), + ) + } + blockNumberField := rawField.FieldByName("BlockNumber") if !walletIDField.IsValid() || @@ -1686,13 +1727,10 @@ func resolveWalletPublicKeyHashForWalletID( bridge any, ) ([20]byte, error) { resolveCanonical, ok := bridge.(walletPublicKeyHashForWalletIDFn) - if !ok { - resolveCanonical = nil - } var walletPublicKeyHash [20]byte var err error - if resolveCanonical != nil { + if ok { walletPublicKeyHash, err = resolveCanonical.WalletPubKeyHashForWalletID(walletID) } else { err = fmt.Errorf("wallet public key hash accessor unavailable") @@ -1706,6 +1744,14 @@ func resolveWalletPublicKeyHashForWalletID( legacyWalletPublicKeyHash, ok := tbtc.WalletPublicKeyHashFromLegacyWalletID(walletID) if ok { + if err != nil { + logger.Infof( + "canonical wallet public key hash resolution failed for wallet ID [0x%x]; using legacy derivation: [%v]", + walletID, + err, + ) + } + return legacyWalletPublicKeyHash, nil } diff --git a/pkg/chain/ethereum/tbtc_test.go b/pkg/chain/ethereum/tbtc_test.go index cf94830ea3..f8b4235b50 100644 --- a/pkg/chain/ethereum/tbtc_test.go +++ b/pkg/chain/ethereum/tbtc_test.go @@ -328,6 +328,105 @@ func TestCalculateWalletID(t *testing.T) { testutils.AssertBytesEqual(t, expectedWalletID[:], actualWalletID[:]) } +type pastNewWalletRegisteredV2EventsBridgeMock struct { + pastEvents func( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPublicKeyHash [][20]byte, + ) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) +} + +func (m *pastNewWalletRegisteredV2EventsBridgeMock) PastNewWalletRegisteredV2Events( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPublicKeyHash [][20]byte, +) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { + return m.pastEvents( + startBlock, + endBlock, + walletID, + ecdsaWalletID, + walletPublicKeyHash, + ) +} + +type pastNewWalletRegisteredV2EventsAltFieldBridgeMock struct { + pastEvents func( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPublicKeyHash [][20]byte, + ) ([]*pastNewWalletRegisteredV2EventsAltFieldEvent, error) +} + +type pastNewWalletRegisteredV2EventsAltFieldEvent struct { + WalletID [32]byte + EcdsaWalletID [32]byte + WalletPublicKeyHash [20]byte + Raw types.Log +} + +func (m *pastNewWalletRegisteredV2EventsAltFieldBridgeMock) PastNewWalletRegisteredV2Events( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPublicKeyHash [][20]byte, +) ([]*pastNewWalletRegisteredV2EventsAltFieldEvent, error) { + return m.pastEvents( + startBlock, + endBlock, + walletID, + ecdsaWalletID, + walletPublicKeyHash, + ) +} + +type pastNewWalletRegisteredV2EventsMissingRawBridgeMock struct { + pastEvents func( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPublicKeyHash [][20]byte, + ) ([]*pastNewWalletRegisteredV2EventsMissingRawEvent, error) +} + +type pastNewWalletRegisteredV2EventsMissingRawEvent struct { + WalletID [32]byte + EcdsaWalletID [32]byte + WalletPubKeyHash [20]byte +} + +func (m *pastNewWalletRegisteredV2EventsMissingRawBridgeMock) PastNewWalletRegisteredV2Events( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + walletPublicKeyHash [][20]byte, +) ([]*pastNewWalletRegisteredV2EventsMissingRawEvent, error) { + return m.pastEvents( + startBlock, + endBlock, + walletID, + ecdsaWalletID, + walletPublicKeyHash, + ) +} + +type pastNewWalletRegisteredV2EventsWrongSignatureBridgeMock struct{} + +func (m *pastNewWalletRegisteredV2EventsWrongSignatureBridgeMock) PastNewWalletRegisteredV2Events( + startBlock uint64, +) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { + return nil, nil +} + func TestPastNewWalletRegisteredEvents_UsesV2EventsWhenAvailable(t *testing.T) { startBlock := uint64(500) endBlock := uint64(700) @@ -349,36 +448,38 @@ func TestPastNewWalletRegisteredEvents_UsesV2EventsWhenAvailable(t *testing.T) { nil, nil, nil, - func( - actualStartBlock uint64, - actualEndBlock *uint64, - _ [][32]byte, - _ [][32]byte, - _ [][20]byte, - ) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { - if actualStartBlock != startBlock { - t.Fatalf("unexpected start block: [%v]", actualStartBlock) - } + &pastNewWalletRegisteredV2EventsBridgeMock{ + pastEvents: func( + actualStartBlock uint64, + actualEndBlock *uint64, + _ [][32]byte, + _ [][32]byte, + _ [][20]byte, + ) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { + if actualStartBlock != startBlock { + t.Fatalf("unexpected start block: [%v]", actualStartBlock) + } - if actualEndBlock == nil || *actualEndBlock != endBlock { - t.Fatalf("unexpected end block: [%v]", actualEndBlock) - } + if actualEndBlock == nil || *actualEndBlock != endBlock { + t.Fatalf("unexpected end block: [%v]", actualEndBlock) + } - // Provide events out of order to verify post-conversion sort. - return []*tbtcabi.BridgeNewWalletRegisteredV2{ - { - WalletID: expectedWalletIDB, - EcdsaWalletID: expectedECDSAWalletIDB, - WalletPubKeyHash: expectedWalletPublicKeyHashB, - Raw: types.Log{BlockNumber: 650}, - }, - { - WalletID: expectedWalletIDA, - EcdsaWalletID: expectedECDSAWalletIDA, - WalletPubKeyHash: expectedWalletPublicKeyHashA, - Raw: types.Log{BlockNumber: 600}, - }, - }, nil + // Provide events out of order to verify post-conversion sort. + return []*tbtcabi.BridgeNewWalletRegisteredV2{ + { + WalletID: expectedWalletIDB, + EcdsaWalletID: expectedECDSAWalletIDB, + WalletPubKeyHash: expectedWalletPublicKeyHashB, + Raw: types.Log{BlockNumber: 650}, + }, + { + WalletID: expectedWalletIDA, + EcdsaWalletID: expectedECDSAWalletIDA, + WalletPubKeyHash: expectedWalletPublicKeyHashA, + Raw: types.Log{BlockNumber: 600}, + }, + }, nil + }, }, func(uint64, *uint64, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegistered, error) { legacyFallbackCalled = true @@ -424,8 +525,16 @@ func TestPastNewWalletRegisteredEvents_FallsBackToLegacyWhenV2Empty(t *testing.T nil, // no canonical wallet-ID filter -> fallback path enabled nil, nil, - func(uint64, *uint64, [][32]byte, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { - return []*tbtcabi.BridgeNewWalletRegisteredV2{}, nil + &pastNewWalletRegisteredV2EventsBridgeMock{ + pastEvents: func( + uint64, + *uint64, + [][32]byte, + [][32]byte, + [][20]byte, + ) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { + return []*tbtcabi.BridgeNewWalletRegisteredV2{}, nil + }, }, func(uint64, *uint64, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegistered, error) { legacyFallbackCalled = true @@ -473,8 +582,16 @@ func TestPastNewWalletRegisteredEvents_DoesNotFallbackWithWalletIDFilter(t *test walletIDFilter, nil, nil, - func(uint64, *uint64, [][32]byte, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { - return []*tbtcabi.BridgeNewWalletRegisteredV2{}, nil + &pastNewWalletRegisteredV2EventsBridgeMock{ + pastEvents: func( + uint64, + *uint64, + [][32]byte, + [][32]byte, + [][20]byte, + ) ([]*tbtcabi.BridgeNewWalletRegisteredV2, error) { + return []*tbtcabi.BridgeNewWalletRegisteredV2{}, nil + }, }, func(uint64, *uint64, [][32]byte, [][20]byte) ([]*tbtcabi.BridgeNewWalletRegistered, error) { legacyFallbackCalled = true @@ -494,6 +611,133 @@ func TestPastNewWalletRegisteredEvents_DoesNotFallbackWithWalletIDFilter(t *test } } +func TestPastNewWalletRegisteredV2Events_ReturnsEmptyWhenMethodUnavailable(t *testing.T) { + actualEvents, err := pastNewWalletRegisteredV2Events( + 1, + nil, + nil, + nil, + nil, + struct{}{}, + ) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if len(actualEvents) != 0 { + t.Fatalf("unexpected events count: [%v]", len(actualEvents)) + } +} + +func TestPastNewWalletRegisteredV2Events_UsesWalletPublicKeyHashFallbackField(t *testing.T) { + expectedWalletID := [32]byte{0x01} + expectedECDSAWalletID := [32]byte{0x02} + expectedWalletPublicKeyHash := [20]byte{0x03} + + actualEvents, err := pastNewWalletRegisteredV2Events( + 11, + nil, + nil, + nil, + nil, + &pastNewWalletRegisteredV2EventsAltFieldBridgeMock{ + pastEvents: func( + uint64, + *uint64, + [][32]byte, + [][32]byte, + [][20]byte, + ) ([]*pastNewWalletRegisteredV2EventsAltFieldEvent, error) { + return []*pastNewWalletRegisteredV2EventsAltFieldEvent{ + { + WalletID: expectedWalletID, + EcdsaWalletID: expectedECDSAWalletID, + WalletPublicKeyHash: expectedWalletPublicKeyHash, + Raw: types.Log{BlockNumber: 121}, + }, + }, nil + }, + }, + ) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if len(actualEvents) != 1 { + t.Fatalf("unexpected events count: [%v]", len(actualEvents)) + } + + if actualEvents[0].WalletPublicKeyHash != expectedWalletPublicKeyHash { + t.Fatalf( + "unexpected wallet public key hash\nexpected: [%x]\nactual: [%x]", + expectedWalletPublicKeyHash, + actualEvents[0].WalletPublicKeyHash, + ) + } +} + +func TestPastNewWalletRegisteredV2Events_ReturnsErrorOnCallPanic(t *testing.T) { + _, err := pastNewWalletRegisteredV2Events( + 1, + nil, + nil, + nil, + nil, + &pastNewWalletRegisteredV2EventsWrongSignatureBridgeMock{}, + ) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "panic calling PastNewWalletRegisteredV2Events") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +func TestPastNewWalletRegisteredV2Events_ReturnsErrorWhenRawMissing(t *testing.T) { + _, err := pastNewWalletRegisteredV2Events( + 1, + nil, + nil, + nil, + nil, + &pastNewWalletRegisteredV2EventsMissingRawBridgeMock{ + pastEvents: func( + uint64, + *uint64, + [][32]byte, + [][32]byte, + [][20]byte, + ) ([]*pastNewWalletRegisteredV2EventsMissingRawEvent, error) { + return []*pastNewWalletRegisteredV2EventsMissingRawEvent{ + { + WalletID: [32]byte{0x05}, + EcdsaWalletID: [32]byte{0x06}, + WalletPubKeyHash: [20]byte{0x07}, + }, + }, nil + }, + }, + ) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), "raw event payload") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +type walletPublicKeyHashForWalletIDBridgeMock struct { + resolve func(walletID [32]byte) ([20]byte, error) +} + +func (m *walletPublicKeyHashForWalletIDBridgeMock) WalletPubKeyHashForWalletID( + walletID [32]byte, +) ([20]byte, error) { + return m.resolve(walletID) +} + func TestResolveWalletPublicKeyHashForWalletID(t *testing.T) { t.Run("returns canonical mapping when non-zero", func(t *testing.T) { walletID := [32]byte{0x01} @@ -501,12 +745,14 @@ func TestResolveWalletPublicKeyHashForWalletID(t *testing.T) { actualWalletPublicKeyHash, err := resolveWalletPublicKeyHashForWalletID( walletID, - func(actualWalletID [32]byte) ([20]byte, error) { - if actualWalletID != walletID { - t.Fatalf("unexpected wallet ID: [%x]", actualWalletID) - } + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: func(actualWalletID [32]byte) ([20]byte, error) { + if actualWalletID != walletID { + t.Fatalf("unexpected wallet ID: [%x]", actualWalletID) + } - return expectedWalletPublicKeyHash, nil + return expectedWalletPublicKeyHash, nil + }, }, ) if err != nil { @@ -528,8 +774,10 @@ func TestResolveWalletPublicKeyHashForWalletID(t *testing.T) { actualWalletPublicKeyHash, err := resolveWalletPublicKeyHashForWalletID( legacyWalletID, - func([32]byte) ([20]byte, error) { - return [20]byte{}, errors.New("canonical lookup unavailable") + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: func([32]byte) ([20]byte, error) { + return [20]byte{}, errors.New("canonical lookup unavailable") + }, }, ) if err != nil { @@ -551,8 +799,10 @@ func TestResolveWalletPublicKeyHashForWalletID(t *testing.T) { actualWalletPublicKeyHash, err := resolveWalletPublicKeyHashForWalletID( legacyWalletID, - func([32]byte) ([20]byte, error) { - return [20]byte{}, nil + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: func([32]byte) ([20]byte, error) { + return [20]byte{}, nil + }, }, ) if err != nil { @@ -574,8 +824,10 @@ func TestResolveWalletPublicKeyHashForWalletID(t *testing.T) { _, err := resolveWalletPublicKeyHashForWalletID( walletID, - func([32]byte) ([20]byte, error) { - return [20]byte{}, canonicalErr + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: func([32]byte) ([20]byte, error) { + return [20]byte{}, canonicalErr + }, }, ) if err == nil { @@ -595,8 +847,10 @@ func TestResolveWalletPublicKeyHashForWalletID(t *testing.T) { _, err := resolveWalletPublicKeyHashForWalletID( walletID, - func([32]byte) ([20]byte, error) { - return [20]byte{}, nil + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: func([32]byte) ([20]byte, error) { + return [20]byte{}, nil + }, }, ) if err == nil { diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go index 5fec7c4b1b..1ee7d20722 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -30,12 +30,12 @@ type NativeTBTCSignerRoundContribution struct { // NativeTBTCSignerRoundState captures coarse session round metadata returned by // StartSignRound. type NativeTBTCSignerRoundState struct { - SessionID string `json:"sessionID"` - RoundID string `json:"roundID"` - RequiredContributions uint16 `json:"requiredContributions"` - MessageDigestHex string `json:"messageDigestHex"` - SigningParticipants []uint16 - OwnContribution *NativeTBTCSignerRoundContribution + SessionID string `json:"sessionID"` + RoundID string `json:"roundID"` + RequiredContributions uint16 `json:"requiredContributions"` + MessageDigestHex string `json:"messageDigestHex"` + SigningParticipants []uint16 `json:"signingParticipants"` + OwnContribution *NativeTBTCSignerRoundContribution `json:"ownContribution"` } // NativeTBTCSignerEngine executes coarse, session-keyed tbtc-signer From a71975e367dbb1205acf9d419d29198162ef0b8c Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 13:52:42 -0600 Subject: [PATCH 69/96] Stabilize tbtc signer tests for native material migration --- pkg/tbtc/marshaling_test.go | 4 +- pkg/tbtc/node_test.go | 4 +- pkg/tbtc/registry_test.go | 18 ++-- pkg/tbtc/signer_equivalence_test.go | 82 +++++++++++++++++++ ...igning_native_backend_frost_native_test.go | 11 ++- 5 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 pkg/tbtc/signer_equivalence_test.go diff --git a/pkg/tbtc/marshaling_test.go b/pkg/tbtc/marshaling_test.go index 57deeb01c4..c1e750f9ec 100644 --- a/pkg/tbtc/marshaling_test.go +++ b/pkg/tbtc/marshaling_test.go @@ -26,9 +26,7 @@ func TestSignerMarshalling(t *testing.T) { if err := pbutils.RoundTrip(marshaled, unmarshaled); err != nil { t.Fatal(err) } - if !reflect.DeepEqual(marshaled, unmarshaled) { - t.Fatal("unexpected content of unmarshaled signer") - } + assertSignerEquivalent(t, "unmarshaled signer", marshaled, unmarshaled) } func TestSignerMarshalling_NonTECDSAKey(t *testing.T) { diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index c1795dd774..967cb79ece 100644 --- a/pkg/tbtc/node_test.go +++ b/pkg/tbtc/node_test.go @@ -100,9 +100,7 @@ func TestNode_GetSigningExecutor(t *testing.T) { len(executor.signers), ) - if !reflect.DeepEqual(signer, executor.signers[0]) { - t.Errorf("executor holds an unexpected signer") - } + assertSignerEquivalent(t, "executor signer", signer, executor.signers[0]) expectedChannel := fmt.Sprintf( "%s-%s", diff --git a/pkg/tbtc/registry_test.go b/pkg/tbtc/registry_test.go index f0d4964ce1..ae5a7ed589 100644 --- a/pkg/tbtc/registry_test.go +++ b/pkg/tbtc/registry_test.go @@ -283,9 +283,12 @@ func TestWalletRegistry_PrePopulateWalletCache(t *testing.T) { len(walletRegistry.walletCache[walletStorageKey].signers), ) - if !reflect.DeepEqual(signer, walletRegistry.walletCache[walletStorageKey].signers[0]) { - t.Errorf("loaded wallet signer differs from the original one") - } + assertSignerEquivalent( + t, + "pre-populated wallet signer", + signer, + walletRegistry.walletCache[walletStorageKey].signers[0], + ) } func TestWalletRegistry_GetWalletsPublicKeys(t *testing.T) { @@ -459,9 +462,12 @@ func TestWalletStorage_LoadSigners(t *testing.T) { len(signersByWallet[walletStorageKey]), ) - if !reflect.DeepEqual(signer, signersByWallet[walletStorageKey][0]) { - t.Errorf("loaded wallet signer differs from the original one") - } + assertSignerEquivalent( + t, + "loaded wallet signer", + signer, + signersByWallet[walletStorageKey][0], + ) } func TestWalletStorage_ArchiveWallet(t *testing.T) { diff --git a/pkg/tbtc/signer_equivalence_test.go b/pkg/tbtc/signer_equivalence_test.go new file mode 100644 index 0000000000..382ba85bd2 --- /dev/null +++ b/pkg/tbtc/signer_equivalence_test.go @@ -0,0 +1,82 @@ +package tbtc + +import ( + "bytes" + "reflect" + "testing" +) + +func assertSignerEquivalent( + t *testing.T, + name string, + expected *signer, + actual *signer, +) { + t.Helper() + + if expected == nil { + if actual != nil { + t.Fatalf("%s should be nil", name) + } + return + } + + if actual == nil { + t.Fatalf("%s is nil", name) + } + + if !expected.wallet.publicKey.Equal(actual.wallet.publicKey) { + t.Fatalf("%s has unexpected wallet public key", name) + } + + if !reflect.DeepEqual( + expected.wallet.signingGroupOperators, + actual.wallet.signingGroupOperators, + ) { + t.Fatalf( + "%s has unexpected signing group operators\nexpected: [%v]\nactual: [%v]", + name, + expected.wallet.signingGroupOperators, + actual.wallet.signingGroupOperators, + ) + } + + if expected.signingGroupMemberIndex != actual.signingGroupMemberIndex { + t.Fatalf( + "%s has unexpected member index\nexpected: [%v]\nactual: [%v]", + name, + expected.signingGroupMemberIndex, + actual.signingGroupMemberIndex, + ) + } + + if expected.privateKeyShare == nil { + if actual.privateKeyShare != nil { + t.Fatalf("%s should have nil private key share", name) + } + return + } + + if actual.privateKeyShare == nil { + t.Fatalf("%s has nil private key share", name) + } + + expectedPrivateKeyShare, err := expected.privateKeyShare.Marshal() + if err != nil { + t.Fatalf("cannot marshal expected private key share for %s: [%v]", name, err) + } + + actualPrivateKeyShare, err := actual.privateKeyShare.Marshal() + if err != nil { + t.Fatalf("cannot marshal actual private key share for %s: [%v]", name, err) + } + + if !bytes.Equal(expectedPrivateKeyShare, actualPrivateKeyShare) { + t.Fatalf( + "%s has unexpected private key share\nexpected: [%x]\nactual: [%x]", + name, + expectedPrivateKeyShare, + actualPrivateKeyShare, + ) + } +} diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index 0a920cf3e5..a1405b4d3c 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -707,13 +707,8 @@ func TestSigningExecutor_Sign_FFIStrictBackend_TBTCSignerPath_AttemptVariationCh t.Cleanup(frostsigning.UnregisterNativeTBTCSignerEngine) t.Cleanup(frostsigning.UnregisterNativeTBTCSignerFallbackObserver) - err := frostsigning.RegisterNativeTBTCSignerEngine(nativeTBTCSignerEngine) - if err != nil { - t.Fatalf("unexpected native tbtc-signer engine registration error: [%v]", err) - } - var fallbackEvents []frostsigning.NativeTBTCSignerFallbackEvent - err = frostsigning.RegisterNativeTBTCSignerFallbackObserver( + err := frostsigning.RegisterNativeTBTCSignerFallbackObserver( func(event frostsigning.NativeTBTCSignerFallbackEvent) { fallbackEvents = append(fallbackEvents, event) }, @@ -727,6 +722,10 @@ func TestSigningExecutor_Sign_FFIStrictBackend_TBTCSignerPath_AttemptVariationCh frostsigning.UnregisterNativeExecutionBridge() frostsigning.UnregisterNativeExecutionFFIExecutor() frostsigning.RegisterNativeExecutionAdapterForBuild() + err = frostsigning.RegisterNativeTBTCSignerEngine(nativeTBTCSignerEngine) + if err != nil { + t.Fatalf("unexpected native tbtc-signer engine registration error: [%v]", err) + } t.Cleanup(frostsigning.ResetExecutionBackend) t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) From baf63ea9fa11a4da0e0b3622483afe69698c670c Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 15:02:36 -0600 Subject: [PATCH 70/96] Switch tbtc-signer bootstrap path to coarse signature output --- ...ffi_primitive_transitional_frost_native.go | 95 ++++++++++----- ...rimitive_transitional_frost_native_test.go | 115 ++++++++++++------ 2 files changed, 142 insertions(+), 68 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index a38e815988..0b56a8a06b 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -302,14 +302,15 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) ) } - if err := executeBuildTaggedTBTCSignerBootstrapCoarseRound( + coarseSignatureBytes, err := executeBuildTaggedTBTCSignerBootstrapCoarseRoundWithSignature( ctx, request, keyGroupForRound, nativeEngine, includedMembersSet, includedMembersIndexes, - ); err != nil { + ) + if err != nil { return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, logger, @@ -320,20 +321,25 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) ) } + coarseSignature, err := decodeBuildTaggedTBTCSignerSignature(coarseSignatureBytes) + if err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf("cannot decode tbtc-signer coarse signature: [%v]", err), + payload.KeyGroupSource, + ) + } + if logger != nil { logger.Debugf( - "validated tbtc-signer key-group contract via RunDKG and bootstrap coarse round; using legacy fallback until signature cutover", + "validated tbtc-signer key-group contract via RunDKG and bootstrap coarse round; returning coarse signature", ) } - return btlcnnefsp.fallbackTBTCSignerLegacySigning( - ctx, - logger, - request, - legacyPrivateKeyShare, - "tbtc-signer bootstrap coarse round completed; using legacy fallback during migration", - payload.KeyGroupSource, - ) + return coarseSignature, nil } func buildTaggedTBTCSignerRunDKGInputs( @@ -482,16 +488,36 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( includedMembersSet map[group.MemberIndex]struct{}, includedMembersIndexes []group.MemberIndex, ) error { + _, err := executeBuildTaggedTBTCSignerBootstrapCoarseRoundWithSignature( + ctx, + request, + keyGroup, + nativeEngine, + includedMembersSet, + includedMembersIndexes, + ) + + return err +} + +func executeBuildTaggedTBTCSignerBootstrapCoarseRoundWithSignature( + ctx context.Context, + request *NativeExecutionFFISigningRequest, + keyGroup string, + nativeEngine NativeTBTCSignerEngine, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, +) ([]byte, error) { if request == nil { - return fmt.Errorf("request is nil") + return nil, fmt.Errorf("request is nil") } if request.Message == nil { - return fmt.Errorf("request message is nil") + return nil, fmt.Errorf("request message is nil") } if nativeEngine == nil { - return fmt.Errorf("native tbtc-signer engine is nil") + return nil, fmt.Errorf("native tbtc-signer engine is nil") } if ctx == nil { @@ -502,12 +528,12 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( var err error includedMembersSet, includedMembersIndexes, err = includedMembersFromRequest(request) if err != nil { - return fmt.Errorf("cannot determine included members: [%w]", err) + return nil, fmt.Errorf("cannot determine included members: [%w]", err) } } if _, ok := includedMembersSet[request.MemberIndex]; !ok { - return fmt.Errorf( + return nil, fmt.Errorf( "member [%v] not included in tbtc-signer signing attempt", request.MemberIndex, ) @@ -519,14 +545,14 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( } if request.MemberIndex == 0 { - return fmt.Errorf("request member index is zero") + return nil, fmt.Errorf("request member index is zero") } signingParticipants, err := buildTaggedTBTCSignerSigningParticipants( includedMembersIndexes, ) if err != nil { - return fmt.Errorf("cannot derive signing participants: [%w]", err) + return nil, fmt.Errorf("cannot derive signing participants: [%w]", err) } roundState, err := nativeEngine.StartSignRound( @@ -537,20 +563,20 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( signingParticipants, ) if err != nil { - return fmt.Errorf("start sign round failed: [%w]", err) + return nil, fmt.Errorf("start sign round failed: [%w]", err) } if roundState == nil { - return fmt.Errorf("start sign round returned nil state") + return nil, fmt.Errorf("start sign round returned nil state") } if roundState.RequiredContributions == 0 { - return fmt.Errorf("start sign round required contributions are zero") + return nil, fmt.Errorf("start sign round required contributions are zero") } if len(signingParticipants) > 0 { if len(roundState.SigningParticipants) != len(signingParticipants) { - return fmt.Errorf( + return nil, fmt.Errorf( "start sign round returned unexpected signing participants count: [%v] != [%v]", len(roundState.SigningParticipants), len(signingParticipants), @@ -559,7 +585,7 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( for i := range signingParticipants { if roundState.SigningParticipants[i] != signingParticipants[i] { - return fmt.Errorf( + return nil, fmt.Errorf( "start sign round returned unexpected signing participant at index [%d]: [%v] != [%v]", i, roundState.SigningParticipants[i], @@ -577,11 +603,11 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( includedMembersIndexes, ) if err != nil { - return fmt.Errorf("cannot collect round contributions: [%w]", err) + return nil, fmt.Errorf("cannot collect round contributions: [%w]", err) } if len(roundContributions) < int(roundState.RequiredContributions) { - return fmt.Errorf( + return nil, fmt.Errorf( "insufficient round contributions: [%v] < [%v]", len(roundContributions), roundState.RequiredContributions, @@ -593,14 +619,27 @@ func executeBuildTaggedTBTCSignerBootstrapCoarseRound( roundContributions, ) if err != nil { - return fmt.Errorf("finalize sign round failed: [%w]", err) + return nil, fmt.Errorf("finalize sign round failed: [%w]", err) } if len(signature) == 0 { - return fmt.Errorf("finalize sign round returned empty signature") + return nil, fmt.Errorf("finalize sign round returned empty signature") } - return nil + return signature, nil +} + +func decodeBuildTaggedTBTCSignerSignature(signature []byte) (*frost.Signature, error) { + if len(signature) == 0 { + return nil, fmt.Errorf("signature is empty") + } + + result := &frost.Signature{} + if err := result.Unmarshal(signature); err != nil { + return nil, fmt.Errorf("invalid frost signature bytes: [%w]", err) + } + + return result, nil } func buildTaggedTBTCSignerSigningParticipants( diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index add0a2c526..5fd184b615 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -254,6 +254,15 @@ func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) finalize return append([]NativeTBTCSignerRoundContribution{}, dbttsbre.finalizeInput...) } +func buildTaggedTBTCSignerValidTestSignature(seed byte) []byte { + signature := make([]byte, 64) + for i := range signature { + signature[i] = seed + byte(i) + } + + return signature +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_ValidatesRequest( t *testing.T, ) { @@ -1408,19 +1417,31 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) { engine := &mockBuildTaggedTBTCSignerEngine{ version: "tbtc-signer/0.1.0-bootstrap", - finalizeSignature: []byte{0xaa}, + finalizeSignature: buildTaggedTBTCSignerValidTestSignature(0x11), } UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) err := RegisterNativeTBTCSignerEngine(engine) if err != nil { t.Fatalf("unexpected registration error: [%v]", err) } + var observedEvents []NativeTBTCSignerFallbackEvent + err = RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + observedEvents = append(observedEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected observer registration error: [%v]", err) + } + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} - _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + signature, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ Message: big.NewInt(123), SessionID: "session-1", MemberIndex: 1, @@ -1431,15 +1452,25 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC Payload: []byte(`{"keyGroup":"group-1"}`), }, }) - if err == nil { - t.Fatal("expected error") + if err != nil { + t.Fatalf("unexpected error: [%v]", err) } - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if signature == nil { + t.Fatal("expected signature") + } + + marshaledSignature, err := signature.Marshal() + if err != nil { + t.Fatalf("cannot marshal signature: [%v]", err) + } + + expectedSignature := buildTaggedTBTCSignerValidTestSignature(0x11) + if !bytes.Equal(marshaledSignature, expectedSignature) { t.Fatalf( - "unexpected error\nexpected: [%v]\nactual: [%v]", - ErrNativeCryptographyUnavailable, - err, + "unexpected signature bytes\nexpected: [%x]\nactual: [%x]", + expectedSignature, + marshaledSignature, ) } @@ -1488,6 +1519,13 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC t.Fatal("expected FinalizeSignRound call in bootstrap tbtc-signer path") } + if len(observedEvents) != 0 { + t.Fatalf( + "did not expect fallback events\nactual: [%v]", + observedEvents, + ) + } + if engine.finalizeSessionID != "session-1" { t.Fatalf( "unexpected FinalizeSignRound session ID\nexpected: [%v]\nactual: [%v]", @@ -1533,7 +1571,7 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC Threshold: 2, CreatedAtUnix: 1, }, - finalizeSignature: []byte{0xaa}, + finalizeSignature: buildTaggedTBTCSignerValidTestSignature(0x22), } UnregisterNativeTBTCSignerEngine() t.Cleanup(UnregisterNativeTBTCSignerEngine) @@ -1545,7 +1583,7 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} - _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + signature, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ Message: big.NewInt(123), SessionID: "session-1", MemberIndex: 1, @@ -1558,15 +1596,25 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ), }, }) - if err == nil { - t.Fatal("expected error") + if err != nil { + t.Fatalf("unexpected error: [%v]", err) } - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if signature == nil { + t.Fatal("expected signature") + } + + marshaledSignature, err := signature.Marshal() + if err != nil { + t.Fatalf("cannot marshal signature: [%v]", err) + } + + expectedSignature := buildTaggedTBTCSignerValidTestSignature(0x22) + if !bytes.Equal(marshaledSignature, expectedSignature) { t.Fatalf( - "unexpected error\nexpected: [%v]\nactual: [%v]", - ErrNativeCryptographyUnavailable, - err, + "unexpected signature bytes\nexpected: [%x]\nactual: [%x]", + expectedSignature, + marshaledSignature, ) } @@ -1854,7 +1902,8 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC var observedSigningParticipants [][]uint16 engine := &mockBuildTaggedTBTCSignerEngine{ - version: "tbtc-signer/0.1.0-bootstrap", + version: "tbtc-signer/0.1.0-bootstrap", + finalizeSignature: buildTaggedTBTCSignerValidTestSignature(0x44), runDKGFn: func( sessionID string, participants []NativeTBTCSignerDKGParticipant, @@ -1931,16 +1980,12 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC }, } - _, err = primitive.Sign(nil, nil, baseRequest) - if err == nil { - t.Fatal("expected first signing error due to legacy fallback without private key share") + firstSignature, err := primitive.Sign(nil, nil, baseRequest) + if err != nil { + t.Fatalf("unexpected first signing error: [%v]", err) } - if !errors.Is(err, ErrNativeCryptographyUnavailable) { - t.Fatalf( - "unexpected first signing error\nexpected: [%v]\nactual: [%v]", - ErrNativeCryptographyUnavailable, - err, - ) + if firstSignature == nil { + t.Fatal("expected first signature") } secondRequest := *baseRequest @@ -1986,28 +2031,18 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } - if len(observedEvents) != 2 { + if len(observedEvents) != 1 { t.Fatalf( "unexpected fallback event count\nexpected: [%d]\nactual: [%d]", - 2, + 1, len(observedEvents), ) } - if !strings.Contains( - observedEvents[0].Reason, - "tbtc-signer bootstrap coarse round completed", - ) { + if !strings.Contains(observedEvents[0].Reason, "session_conflict") { t.Fatalf( - "expected first fallback reason to include bootstrap completion\nactual: [%s]", + "expected fallback reason to include session_conflict\nactual: [%s]", observedEvents[0].Reason, ) } - - if !strings.Contains(observedEvents[1].Reason, "session_conflict") { - t.Fatalf( - "expected second fallback reason to include session_conflict\nactual: [%s]", - observedEvents[1].Reason, - ) - } } From 71b33ec8d914e7310221b91d62bd63f0e5970916 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 15:22:54 -0600 Subject: [PATCH 71/96] Document decode validation boundary and test decode-fallback path --- ...ffi_primitive_transitional_frost_native.go | 3 + ...rimitive_transitional_frost_native_test.go | 83 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 0b56a8a06b..6a5115cdba 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -634,6 +634,9 @@ func decodeBuildTaggedTBTCSignerSignature(signature []byte) (*frost.Signature, e return nil, fmt.Errorf("signature is empty") } + // Unmarshal validates signature wire format (length + split into R/S) only. + // Cryptographic validity is enforced by downstream Schnorr verification at + // submission time. result := &frost.Signature{} if err := result.Unmarshal(signature); err != nil { return nil, fmt.Errorf("invalid frost signature bytes: [%w]", err) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 5fd184b615..787952c355 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -1559,6 +1559,89 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC } } +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_InvalidCoarseSignatureFallsBack( + t *testing.T, +) { + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + finalizeSignature: []byte{0xaa}, + } + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + var observedEvents []NativeTBTCSignerFallbackEvent + err = RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + observedEvents = append(observedEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected observer registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":"group-1","keyGroupSource":"legacy-wallet-pubkey"}`), + }, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if !engine.runDKGCalled { + t.Fatal("expected RunDKG call in bootstrap path") + } + + if !engine.startCalled { + t.Fatal("expected StartSignRound call in bootstrap path") + } + + if !engine.finalizeCalled { + t.Fatal("expected FinalizeSignRound call in bootstrap path") + } + + if len(observedEvents) != 1 { + t.Fatalf( + "unexpected fallback event count\nexpected: [%d]\nactual: [%d]", + 1, + len(observedEvents), + ) + } + + if !strings.Contains( + observedEvents[0].Reason, + "cannot decode tbtc-signer coarse signature", + ) { + t.Fatalf( + "expected fallback reason to include decode failure\nactual: [%s]", + observedEvents[0].Reason, + ) + } +} + func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_LegacyKeyGroupSourceUsesRunDKGResult( t *testing.T, ) { From 346e87bff886e305b1c6cba6bd410be9ea8dd890 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 16:12:20 -0600 Subject: [PATCH 72/96] Add coarse-signature success telemetry --- ...ffi_primitive_transitional_frost_native.go | 14 +- ...rimitive_transitional_frost_native_test.go | 127 ++++++++++++++++++ ..._tbtc_signer_coarse_signature_telemetry.go | 65 +++++++++ ..._signer_coarse_signature_telemetry_test.go | 53 ++++++++ 4 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go create mode 100644 pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 6a5115cdba..75c20e16ce 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -32,9 +32,9 @@ func defaultNativeExecutionFFISigningPrimitiveProviderForBuild() ( // buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive is a // transitional primitive that executes native two-round FROST when // `frost-uniffi-v2` signer material is provided, and preserves legacy bridge -// execution for `frost-uniffi-v1` payloads. `frost-tbtc-signer-v1` currently -// routes through a temporary legacy fallback until coarse session finalize flow -// is wired end-to-end. +// execution for `frost-uniffi-v1` payloads. `frost-tbtc-signer-v1` uses the +// coarse signing flow for bootstrap engine versions and falls back to legacy +// signing for unsupported or failed coarse-path executions. type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} const buildTaggedTBTCSignerVersionPrefix = "tbtc-signer/" @@ -339,6 +339,14 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) ) } + emitNativeTBTCSignerCoarseSignatureEvent( + NativeTBTCSignerCoarseSignatureEvent{ + SessionID: request.SessionID, + KeyGroupSource: payload.KeyGroupSource, + EngineVersion: engineVersion, + }, + ) + return coarseSignature, nil } diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 787952c355..47e1b592b9 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -1421,8 +1421,10 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC } UnregisterNativeTBTCSignerEngine() UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() t.Cleanup(UnregisterNativeTBTCSignerEngine) t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) err := RegisterNativeTBTCSignerEngine(engine) if err != nil { @@ -1439,6 +1441,19 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC t.Fatalf("unexpected observer registration error: [%v]", err) } + var observedCoarseSignatureEvents []NativeTBTCSignerCoarseSignatureEvent + err = RegisterNativeTBTCSignerCoarseSignatureObserver( + func(event NativeTBTCSignerCoarseSignatureEvent) { + observedCoarseSignatureEvents = append( + observedCoarseSignatureEvents, + event, + ) + }, + ) + if err != nil { + t.Fatalf("unexpected coarse observer registration error: [%v]", err) + } + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} signature, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ @@ -1526,6 +1541,30 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } + if len(observedCoarseSignatureEvents) != 1 { + t.Fatalf( + "unexpected coarse signature event count\nexpected: [%d]\nactual: [%d]", + 1, + len(observedCoarseSignatureEvents), + ) + } + + if observedCoarseSignatureEvents[0].SessionID != "session-1" { + t.Fatalf( + "unexpected coarse signature session ID\nexpected: [%s]\nactual: [%s]", + "session-1", + observedCoarseSignatureEvents[0].SessionID, + ) + } + + if observedCoarseSignatureEvents[0].EngineVersion != "tbtc-signer/0.1.0-bootstrap" { + t.Fatalf( + "unexpected coarse signature engine version\nexpected: [%s]\nactual: [%s]", + "tbtc-signer/0.1.0-bootstrap", + observedCoarseSignatureEvents[0].EngineVersion, + ) + } + if engine.finalizeSessionID != "session-1" { t.Fatalf( "unexpected FinalizeSignRound session ID\nexpected: [%v]\nactual: [%v]", @@ -1568,8 +1607,10 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC } UnregisterNativeTBTCSignerEngine() UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() t.Cleanup(UnregisterNativeTBTCSignerEngine) t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) err := RegisterNativeTBTCSignerEngine(engine) if err != nil { @@ -1586,6 +1627,19 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC t.Fatalf("unexpected observer registration error: [%v]", err) } + var observedCoarseSignatureEvents []NativeTBTCSignerCoarseSignatureEvent + err = RegisterNativeTBTCSignerCoarseSignatureObserver( + func(event NativeTBTCSignerCoarseSignatureEvent) { + observedCoarseSignatureEvents = append( + observedCoarseSignatureEvents, + event, + ) + }, + ) + if err != nil { + t.Fatalf("unexpected coarse observer registration error: [%v]", err) + } + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ @@ -1640,6 +1694,13 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC observedEvents[0].Reason, ) } + + if len(observedCoarseSignatureEvents) != 0 { + t.Fatalf( + "did not expect coarse signature events\nactual: [%v]", + observedCoarseSignatureEvents, + ) + } } func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_LegacyKeyGroupSourceUsesRunDKGResult( @@ -1657,13 +1718,28 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC finalizeSignature: buildTaggedTBTCSignerValidTestSignature(0x22), } UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerCoarseSignatureObserver() t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) err := RegisterNativeTBTCSignerEngine(engine) if err != nil { t.Fatalf("unexpected registration error: [%v]", err) } + var observedCoarseSignatureEvents []NativeTBTCSignerCoarseSignatureEvent + err = RegisterNativeTBTCSignerCoarseSignatureObserver( + func(event NativeTBTCSignerCoarseSignatureEvent) { + observedCoarseSignatureEvents = append( + observedCoarseSignatureEvents, + event, + ) + }, + ) + if err != nil { + t.Fatalf("unexpected coarse observer registration error: [%v]", err) + } + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} signature, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ @@ -1725,6 +1801,30 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC if !engine.finalizeCalled { t.Fatal("expected FinalizeSignRound call in bootstrap path") } + + if len(observedCoarseSignatureEvents) != 1 { + t.Fatalf( + "unexpected coarse signature event count\nexpected: [%d]\nactual: [%d]", + 1, + len(observedCoarseSignatureEvents), + ) + } + + if observedCoarseSignatureEvents[0].KeyGroupSource != "legacy-wallet-pubkey" { + t.Fatalf( + "unexpected coarse signature key group source\nexpected: [%s]\nactual: [%s]", + "legacy-wallet-pubkey", + observedCoarseSignatureEvents[0].KeyGroupSource, + ) + } + + if observedCoarseSignatureEvents[0].EngineVersion != "tbtc-signer/0.1.0-bootstrap" { + t.Fatalf( + "unexpected coarse signature engine version\nexpected: [%s]\nactual: [%s]", + "tbtc-signer/0.1.0-bootstrap", + observedCoarseSignatureEvents[0].EngineVersion, + ) + } } func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_KeyGroupMismatchNonLegacySourceSkipsCoarseRound( @@ -1789,8 +1889,10 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) { UnregisterNativeTBTCSignerEngine() UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() t.Cleanup(UnregisterNativeTBTCSignerEngine) t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) var observedEvents []NativeTBTCSignerFallbackEvent err := RegisterNativeTBTCSignerFallbackObserver( @@ -1859,8 +1961,10 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) { UnregisterNativeTBTCSignerEngine() UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() t.Cleanup(UnregisterNativeTBTCSignerEngine) t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) var firstParticipants []NativeTBTCSignerDKGParticipant engine := &mockBuildTaggedTBTCSignerEngine{ @@ -1978,8 +2082,10 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) { UnregisterNativeTBTCSignerEngine() UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() t.Cleanup(UnregisterNativeTBTCSignerEngine) t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) var firstSigningParticipants []uint16 var observedSigningParticipants [][]uint16 @@ -2049,6 +2155,19 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC t.Fatalf("unexpected observer registration error: [%v]", err) } + var observedCoarseSignatureEvents []NativeTBTCSignerCoarseSignatureEvent + err = RegisterNativeTBTCSignerCoarseSignatureObserver( + func(event NativeTBTCSignerCoarseSignatureEvent) { + observedCoarseSignatureEvents = append( + observedCoarseSignatureEvents, + event, + ) + }, + ) + if err != nil { + t.Fatalf("unexpected coarse observer registration error: [%v]", err) + } + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} baseRequest := &NativeExecutionFFISigningRequest{ @@ -2128,4 +2247,12 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC observedEvents[0].Reason, ) } + + if len(observedCoarseSignatureEvents) != 1 { + t.Fatalf( + "unexpected coarse signature event count\nexpected: [%d]\nactual: [%d]", + 1, + len(observedCoarseSignatureEvents), + ) + } } diff --git a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go new file mode 100644 index 0000000000..adb30bb310 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go @@ -0,0 +1,65 @@ +package signing + +import ( + "fmt" + "sync" +) + +// NativeTBTCSignerCoarseSignatureEvent describes successful coarse-path +// signature production for tbtc-signer payloads. +type NativeTBTCSignerCoarseSignatureEvent struct { + SessionID string + KeyGroupSource string + EngineVersion string +} + +// NativeTBTCSignerCoarseSignatureObserver consumes coarse-signature telemetry +// events. +type NativeTBTCSignerCoarseSignatureObserver func( + event NativeTBTCSignerCoarseSignatureEvent, +) + +var ( + nativeTBTCSignerCoarseSignatureObserverMutex sync.RWMutex + nativeTBTCSignerCoarseSignatureObserver NativeTBTCSignerCoarseSignatureObserver +) + +// RegisterNativeTBTCSignerCoarseSignatureObserver registers a process-wide +// observer used to report tbtc-signer coarse-signature success events. +func RegisterNativeTBTCSignerCoarseSignatureObserver( + observer NativeTBTCSignerCoarseSignatureObserver, +) error { + if observer == nil { + return fmt.Errorf("native tbtc-signer coarse signature observer is nil") + } + + nativeTBTCSignerCoarseSignatureObserverMutex.Lock() + defer nativeTBTCSignerCoarseSignatureObserverMutex.Unlock() + + nativeTBTCSignerCoarseSignatureObserver = observer + + return nil +} + +// UnregisterNativeTBTCSignerCoarseSignatureObserver clears coarse-signature +// observer registration. +func UnregisterNativeTBTCSignerCoarseSignatureObserver() { + nativeTBTCSignerCoarseSignatureObserverMutex.Lock() + defer nativeTBTCSignerCoarseSignatureObserverMutex.Unlock() + + nativeTBTCSignerCoarseSignatureObserver = nil +} + +func emitNativeTBTCSignerCoarseSignatureEvent( + event NativeTBTCSignerCoarseSignatureEvent, +) { + nativeTBTCSignerCoarseSignatureObserverMutex.RLock() + observer := nativeTBTCSignerCoarseSignatureObserver + nativeTBTCSignerCoarseSignatureObserverMutex.RUnlock() + + if observer == nil { + return + } + + observer(event) +} diff --git a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go new file mode 100644 index 0000000000..740b726a51 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go @@ -0,0 +1,53 @@ +package signing + +import "testing" + +func TestRegisterNativeTBTCSignerCoarseSignatureObserverRejectsNil(t *testing.T) { + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + err := RegisterNativeTBTCSignerCoarseSignatureObserver(nil) + if err == nil { + t.Fatal("expected registration error") + } +} + +func TestEmitNativeTBTCSignerCoarseSignatureEvent(t *testing.T) { + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + var ( + received bool + actual NativeTBTCSignerCoarseSignatureEvent + ) + + err := RegisterNativeTBTCSignerCoarseSignatureObserver( + func(event NativeTBTCSignerCoarseSignatureEvent) { + received = true + actual = event + }, + ) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + expected := NativeTBTCSignerCoarseSignatureEvent{ + SessionID: "session-1", + KeyGroupSource: "legacy-wallet-pubkey", + EngineVersion: "tbtc-signer/0.1.0-bootstrap", + } + + emitNativeTBTCSignerCoarseSignatureEvent(expected) + + if !received { + t.Fatal("expected coarse signature event to be delivered") + } + + if actual != expected { + t.Fatalf( + "unexpected coarse signature event\nexpected: [%+v]\nactual: [%+v]", + expected, + actual, + ) + } +} From c14712a5d76c8ae2a4170d9fca3b4eebb5a093a3 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 16:40:27 -0600 Subject: [PATCH 73/96] Document coarse observer scope and nil-observer no-op --- ...native_tbtc_signer_coarse_signature_telemetry.go | 2 ++ ...e_tbtc_signer_coarse_signature_telemetry_test.go | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go index adb30bb310..ce8e5f739a 100644 --- a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go +++ b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go @@ -26,6 +26,8 @@ var ( // RegisterNativeTBTCSignerCoarseSignatureObserver registers a process-wide // observer used to report tbtc-signer coarse-signature success events. +// Only a single observer is supported; a subsequent registration replaces the +// existing observer. func RegisterNativeTBTCSignerCoarseSignatureObserver( observer NativeTBTCSignerCoarseSignatureObserver, ) error { diff --git a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go index 740b726a51..e940b93f43 100644 --- a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go +++ b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go @@ -51,3 +51,16 @@ func TestEmitNativeTBTCSignerCoarseSignatureEvent(t *testing.T) { ) } } + +func TestEmitNativeTBTCSignerCoarseSignatureEventWithoutObserver(t *testing.T) { + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + emitNativeTBTCSignerCoarseSignatureEvent( + NativeTBTCSignerCoarseSignatureEvent{ + SessionID: "session-1", + KeyGroupSource: "legacy-wallet-pubkey", + EngineVersion: "tbtc-signer/0.1.0-bootstrap", + }, + ) +} From 98301c92c63a55d540e44dc74dcffa89eb0b079e Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 17:41:56 -0600 Subject: [PATCH 74/96] Guard tbtc-signer telemetry observer re-registration --- ..._tbtc_signer_coarse_signature_telemetry.go | 9 +++++++-- ..._signer_coarse_signature_telemetry_test.go | 19 +++++++++++++++++++ .../native_tbtc_signer_fallback_telemetry.go | 5 +++++ ...ive_tbtc_signer_fallback_telemetry_test.go | 19 +++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go index ce8e5f739a..d406baed0c 100644 --- a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go +++ b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go @@ -26,8 +26,7 @@ var ( // RegisterNativeTBTCSignerCoarseSignatureObserver registers a process-wide // observer used to report tbtc-signer coarse-signature success events. -// Only a single observer is supported; a subsequent registration replaces the -// existing observer. +// Only a single observer is supported. func RegisterNativeTBTCSignerCoarseSignatureObserver( observer NativeTBTCSignerCoarseSignatureObserver, ) error { @@ -38,6 +37,12 @@ func RegisterNativeTBTCSignerCoarseSignatureObserver( nativeTBTCSignerCoarseSignatureObserverMutex.Lock() defer nativeTBTCSignerCoarseSignatureObserverMutex.Unlock() + if nativeTBTCSignerCoarseSignatureObserver != nil { + return fmt.Errorf( + "native tbtc-signer coarse signature observer is already registered", + ) + } + nativeTBTCSignerCoarseSignatureObserver = observer return nil diff --git a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go index e940b93f43..5c59d3a020 100644 --- a/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go +++ b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go @@ -12,6 +12,25 @@ func TestRegisterNativeTBTCSignerCoarseSignatureObserverRejectsNil(t *testing.T) } } +func TestRegisterNativeTBTCSignerCoarseSignatureObserverRejectsDuplicate(t *testing.T) { + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + firstErr := RegisterNativeTBTCSignerCoarseSignatureObserver( + func(NativeTBTCSignerCoarseSignatureEvent) {}, + ) + if firstErr != nil { + t.Fatalf("unexpected first registration error: [%v]", firstErr) + } + + secondErr := RegisterNativeTBTCSignerCoarseSignatureObserver( + func(NativeTBTCSignerCoarseSignatureEvent) {}, + ) + if secondErr == nil { + t.Fatal("expected duplicate registration error") + } +} + func TestEmitNativeTBTCSignerCoarseSignatureEvent(t *testing.T) { UnregisterNativeTBTCSignerCoarseSignatureObserver() t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) diff --git a/pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go index 09ee08054d..82a1469ffa 100644 --- a/pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go +++ b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go @@ -24,6 +24,7 @@ var ( // RegisterNativeTBTCSignerFallbackObserver registers a process-wide observer // used to report tbtc-signer fallback events. +// Only a single observer is supported. func RegisterNativeTBTCSignerFallbackObserver( observer NativeTBTCSignerFallbackObserver, ) error { @@ -34,6 +35,10 @@ func RegisterNativeTBTCSignerFallbackObserver( nativeTBTCSignerFallbackObserverMutex.Lock() defer nativeTBTCSignerFallbackObserverMutex.Unlock() + if nativeTBTCSignerFallbackObserver != nil { + return fmt.Errorf("native tbtc-signer fallback observer is already registered") + } + nativeTBTCSignerFallbackObserver = observer return nil diff --git a/pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go index 45d8039bf2..457b9710d2 100644 --- a/pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go +++ b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go @@ -14,6 +14,25 @@ func TestRegisterNativeTBTCSignerFallbackObserverRejectsNil(t *testing.T) { } } +func TestRegisterNativeTBTCSignerFallbackObserverRejectsDuplicate(t *testing.T) { + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + firstErr := RegisterNativeTBTCSignerFallbackObserver( + func(NativeTBTCSignerFallbackEvent) {}, + ) + if firstErr != nil { + t.Fatalf("unexpected first registration error: [%v]", firstErr) + } + + secondErr := RegisterNativeTBTCSignerFallbackObserver( + func(NativeTBTCSignerFallbackEvent) {}, + ) + if secondErr == nil { + t.Fatal("expected duplicate registration error") + } +} + func TestEmitNativeTBTCSignerFallbackEvent(t *testing.T) { UnregisterNativeTBTCSignerFallbackObserver() t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) From 1b483cd634a2810d8e2ac1a29eab4690c4dc2598 Mon Sep 17 00:00:00 2001 From: maclane Date: Tue, 24 Feb 2026 21:14:23 -0600 Subject: [PATCH 75/96] Wire BuildTaprootTx through keep-core wallet orchestration --- pkg/bitcoin/transaction_builder.go | 66 ++++++ pkg/bitcoin/transaction_builder_test.go | 97 ++++++++ ...rimitive_transitional_frost_native_test.go | 18 ++ ...e_tbtc_signer_registration_frost_native.go | 216 ++++++++++++++++++ ...c_signer_registration_frost_native_test.go | 215 +++++++++++++++++ ...tc_signer_build_taproot_tx_frost_native.go | 36 +++ .../native_tbtc_signer_engine_frost_native.go | 28 +++ ...ve_tbtc_signer_engine_frost_native_test.go | 9 + ...ve_tbtc_signer_build_taproot_tx_default.go | 13 ++ ...ild_taproot_tx_frost_native_tbtc_signer.go | 104 +++++++++ ...igning_native_backend_frost_native_test.go | 9 + pkg/tbtc/wallet.go | 17 ++ ..._sign_transaction_build_taproot_tx_test.go | 35 +++ 13 files changed, 863 insertions(+) create mode 100644 pkg/frost/signing/native_tbtc_signer_build_taproot_tx_frost_native.go create mode 100644 pkg/tbtc/native_tbtc_signer_build_taproot_tx_default.go create mode 100644 pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go create mode 100644 pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go diff --git a/pkg/bitcoin/transaction_builder.go b/pkg/bitcoin/transaction_builder.go index e446f07517..852179b6ca 100644 --- a/pkg/bitcoin/transaction_builder.go +++ b/pkg/bitcoin/transaction_builder.go @@ -2,6 +2,7 @@ package bitcoin import ( "crypto/ecdsa" + "encoding/hex" "fmt" "math/big" @@ -309,6 +310,71 @@ func (tb *TransactionBuilder) TotalInputsValue() int64 { return totalInputsValue } +// UnsignedTransactionInput carries canonical unsigned input metadata extracted +// from the builder state. +type UnsignedTransactionInput struct { + TxIDHex string + Vout uint32 + ValueSats uint64 +} + +// UnsignedTransactionOutput carries canonical unsigned output metadata +// extracted from the builder state. +type UnsignedTransactionOutput struct { + ScriptPubKeyHex string + ValueSats uint64 +} + +// UnsignedTransactionIO returns canonical unsigned transaction input/output +// metadata from the builder state. +func (tb *TransactionBuilder) UnsignedTransactionIO() ( + []UnsignedTransactionInput, + []UnsignedTransactionOutput, + error, +) { + if len(tb.internal.TxIn) != len(tb.sigHashArgs) { + return nil, nil, fmt.Errorf( + "input metadata mismatch: [%d] tx inputs, [%d] sighash args", + len(tb.internal.TxIn), + len(tb.sigHashArgs), + ) + } + + inputs := make([]UnsignedTransactionInput, 0, len(tb.internal.TxIn)) + for i, input := range tb.internal.TxIn { + value := tb.sigHashArgs[i].value + if value < 0 { + return nil, nil, fmt.Errorf("input [%d] value is negative", i) + } + + inputs = append( + inputs, + UnsignedTransactionInput{ + TxIDHex: input.PreviousOutPoint.Hash.String(), + Vout: input.PreviousOutPoint.Index, + ValueSats: uint64(value), + }, + ) + } + + outputs := make([]UnsignedTransactionOutput, 0, len(tb.internal.TxOut)) + for i, output := range tb.internal.TxOut { + if output.Value < 0 { + return nil, nil, fmt.Errorf("output [%d] value is negative", i) + } + + outputs = append( + outputs, + UnsignedTransactionOutput{ + ScriptPubKeyHex: hex.EncodeToString(output.PkScript), + ValueSats: uint64(output.Value), + }, + ) + } + + return inputs, outputs, nil +} + // inputSigHashArgs is a helper structure holding some arguments required to // compute a sighash for the given input. type inputSigHashArgs struct { diff --git a/pkg/bitcoin/transaction_builder_test.go b/pkg/bitcoin/transaction_builder_test.go index 246e70cd51..a911363cde 100644 --- a/pkg/bitcoin/transaction_builder_test.go +++ b/pkg/bitcoin/transaction_builder_test.go @@ -6,6 +6,8 @@ import ( "reflect" "testing" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" "github.com/keep-network/keep-core/internal/testutils" ) @@ -215,6 +217,101 @@ func TestTransactionBuilder_AddOutput(t *testing.T) { assertInternalOutput(t, builder, 0, output) } +func TestTransactionBuilder_UnsignedTransactionIO(t *testing.T) { + builder := NewTransactionBuilder(nil) + + var txHash chainhash.Hash + for i := range txHash { + txHash[i] = 0x11 + } + + builder.internal.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, 7), nil, nil)) + builder.sigHashArgs = append(builder.sigHashArgs, &inputSigHashArgs{value: 1234}) + builder.AddOutput(&TransactionOutput{ + Value: 1000, + PublicKeyScript: hexToSlice(t, "0014deadbeef"), + }) + + inputs, outputs, err := builder.UnsignedTransactionIO() + if err != nil { + t.Fatalf("unexpected extraction error: [%v]", err) + } + + if len(inputs) != 1 { + t.Fatalf("unexpected input count: [%d]", len(inputs)) + } + + if inputs[0].TxIDHex != txHash.String() { + t.Fatalf( + "unexpected input txid\nexpected: [%v]\nactual: [%v]", + txHash.String(), + inputs[0].TxIDHex, + ) + } + + if inputs[0].Vout != 7 { + t.Fatalf("unexpected input vout: [%d]", inputs[0].Vout) + } + + if inputs[0].ValueSats != 1234 { + t.Fatalf("unexpected input value: [%d]", inputs[0].ValueSats) + } + + if len(outputs) != 1 { + t.Fatalf("unexpected output count: [%d]", len(outputs)) + } + + if outputs[0].ScriptPubKeyHex != "0014deadbeef" { + t.Fatalf( + "unexpected output script\nexpected: [%v]\nactual: [%v]", + "0014deadbeef", + outputs[0].ScriptPubKeyHex, + ) + } + + if outputs[0].ValueSats != 1000 { + t.Fatalf("unexpected output value: [%d]", outputs[0].ValueSats) + } +} + +func TestTransactionBuilder_UnsignedTransactionIO_RejectsNegativeInputValue( + t *testing.T, +) { + builder := NewTransactionBuilder(nil) + + var txHash chainhash.Hash + builder.internal.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, 0), nil, nil)) + builder.sigHashArgs = append(builder.sigHashArgs, &inputSigHashArgs{value: -1}) + builder.AddOutput(&TransactionOutput{ + Value: 1, + PublicKeyScript: hexToSlice(t, "0014aa"), + }) + + _, _, err := builder.UnsignedTransactionIO() + if err == nil { + t.Fatal("expected extraction error") + } +} + +func TestTransactionBuilder_UnsignedTransactionIO_RejectsNegativeOutputValue( + t *testing.T, +) { + builder := NewTransactionBuilder(nil) + + var txHash chainhash.Hash + builder.internal.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, 0), nil, nil)) + builder.sigHashArgs = append(builder.sigHashArgs, &inputSigHashArgs{value: 1}) + builder.AddOutput(&TransactionOutput{ + Value: -1, + PublicKeyScript: hexToSlice(t, "0014aa"), + }) + + _, _, err := builder.UnsignedTransactionIO() + if err == nil { + t.Fatal("expected extraction error") + } +} + // The goal of this test is making sure that the TransactionBuilder can // produce proper signature hashes and apply signatures for all input types, // i.e. P2PKH, P2WPKH, P2SH, and P2WSH. This test uses transactions that diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 47e1b592b9..9874186f3b 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -168,6 +168,15 @@ func (mbttse *mockBuildTaggedTBTCSignerEngine) FinalizeSignRound( return []byte{0xaa}, nil } +func (mbttse *mockBuildTaggedTBTCSignerEngine) BuildTaprootTx( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, +) (*NativeTBTCSignerTxResult, error) { + return nil, errors.New("not implemented") +} + type deterministicBuildTaggedTBTCSignerBootstrapRoundEngine struct { roundState *NativeTBTCSignerRoundState finalizeMutex sync.Mutex @@ -247,6 +256,15 @@ func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) Finalize return []byte{0xaa}, nil } +func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) BuildTaprootTx( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, +) (*NativeTBTCSignerTxResult, error) { + return nil, errors.New("not implemented") +} + func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) finalizeInputs() []NativeTBTCSignerRoundContribution { dbttsbre.finalizeMutex.Lock() defer dbttsbre.finalizeMutex.Unlock() diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index 05230ebc8e..bbec2a369f 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -34,6 +34,10 @@ typedef TbtcSignerResult (*tbtc_finalize_sign_round_fn)( const uint8_t* request_ptr, size_t request_len ); +typedef TbtcSignerResult (*tbtc_build_taproot_tx_fn)( + const uint8_t* request_ptr, + size_t request_len +); typedef void (*tbtc_free_buffer_fn)(uint8_t* ptr, size_t len); static TbtcSignerResult unavailable_tbtc_signer_result(void) { @@ -92,6 +96,18 @@ static TbtcSignerResult tbtc_signer_finalize_sign_round(const uint8_t* request_p return finalize_sign_round(request_ptr, request_len); } +static TbtcSignerResult tbtc_signer_build_taproot_tx(const uint8_t* request_ptr, size_t request_len) { + tbtc_build_taproot_tx_fn build_taproot_tx = (tbtc_build_taproot_tx_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_build_taproot_tx" + ); + if (build_taproot_tx == NULL) { + return unavailable_tbtc_signer_result(); + } + + return build_taproot_tx(request_ptr, request_len); +} + static void tbtc_signer_free_buffer(uint8_t* ptr, size_t len) { tbtc_free_buffer_fn free_buffer = (tbtc_free_buffer_fn)dlsym( RTLD_DEFAULT, @@ -169,6 +185,29 @@ type buildTaggedTBTCSignerFinalizeSignRoundResponse struct { SignatureHex string `json:"signature_hex"` } +type buildTaggedTBTCSignerBuildTaprootTxRequest struct { + SessionID string `json:"session_id"` + Inputs []buildTaggedTBTCSignerBuildTaprootTxInput `json:"inputs"` + Outputs []buildTaggedTBTCSignerBuildTaprootTxOutput `json:"outputs"` + ScriptTreeHex *string `json:"script_tree_hex,omitempty"` +} + +type buildTaggedTBTCSignerBuildTaprootTxInput struct { + TxIDHex string `json:"txid_hex"` + Vout uint32 `json:"vout"` + ValueSats uint64 `json:"value_sats"` +} + +type buildTaggedTBTCSignerBuildTaprootTxOutput struct { + ScriptPubKeyHex string `json:"script_pubkey_hex"` + ValueSats uint64 `json:"value_sats"` +} + +type buildTaggedTBTCSignerBuildTaprootTxResponse struct { + SessionID string `json:"session_id"` + TxHex string `json:"tx_hex"` +} + const buildTaggedTBTCSignerUnavailableStatusCode = -1 func registerBuildTaggedNativeFROSTSigningEngine() error { @@ -260,6 +299,30 @@ func (bttse *buildTaggedTBTCSignerEngine) FinalizeSignRound( return decodeBuildTaggedTBTCSignerFinalizeSignRoundResponse(responsePayload) } +func (bttse *buildTaggedTBTCSignerEngine) BuildTaprootTx( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, +) (*NativeTBTCSignerTxResult, error) { + requestPayload, err := buildTaggedTBTCSignerBuildTaprootTxRequestPayload( + sessionID, + inputs, + outputs, + scriptTreeHex, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerBuildTaprootTx(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerBuildTaprootTxResponse(responsePayload) +} + func buildTaggedTBTCSignerUnavailableError(operation string) error { return fmt.Errorf( "%w: tbtc-signer bridge operation [%v] is unavailable; link libfrost_tbtc", @@ -647,6 +710,147 @@ func decodeBuildTaggedTBTCSignerFinalizeSignRoundResponse( return signature, nil } +func buildTaggedTBTCSignerBuildTaprootTxRequestPayload( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, +) ([]byte, error) { + if sessionID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + "session ID is empty", + ) + } + + if len(inputs) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + "inputs are empty", + ) + } + + if len(outputs) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + "outputs are empty", + ) + } + + requestInputs := make( + []buildTaggedTBTCSignerBuildTaprootTxInput, + 0, + len(inputs), + ) + for i, input := range inputs { + if input.TxIDHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + fmt.Sprintf("input [%d] txid hex is empty", i), + ) + } + + requestInputs = append( + requestInputs, + buildTaggedTBTCSignerBuildTaprootTxInput{ + TxIDHex: input.TxIDHex, + Vout: input.Vout, + ValueSats: input.ValueSats, + }, + ) + } + + requestOutputs := make( + []buildTaggedTBTCSignerBuildTaprootTxOutput, + 0, + len(outputs), + ) + for i, output := range outputs { + if output.ScriptPubKeyHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + fmt.Sprintf("output [%d] script pubkey hex is empty", i), + ) + } + + requestOutputs = append( + requestOutputs, + buildTaggedTBTCSignerBuildTaprootTxOutput{ + ScriptPubKeyHex: output.ScriptPubKeyHex, + ValueSats: output.ValueSats, + }, + ) + } + + var requestScriptTreeHex *string + if scriptTreeHex != nil { + if *scriptTreeHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + "script tree hex is empty", + ) + } + + copied := *scriptTreeHex + requestScriptTreeHex = &copied + } + + request := buildTaggedTBTCSignerBuildTaprootTxRequest{ + SessionID: sessionID, + Inputs: requestInputs, + Outputs: requestOutputs, + ScriptTreeHex: requestScriptTreeHex, + } + + payload, err := json.Marshal(request) + if err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + fmt.Sprintf("cannot marshal request: %v", err), + ) + } + + return payload, nil +} + +func decodeBuildTaggedTBTCSignerBuildTaprootTxResponse( + responsePayload []byte, +) (*NativeTBTCSignerTxResult, error) { + var response buildTaggedTBTCSignerBuildTaprootTxResponse + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + if response.SessionID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + "response session ID is empty", + ) + } + + if response.TxHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + "response tx hex is empty", + ) + } + + if _, err := hex.DecodeString(response.TxHex); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "BuildTaprootTx", + fmt.Sprintf("response tx hex is invalid: %v", err), + ) + } + + return &NativeTBTCSignerTxResult{ + SessionID: response.SessionID, + TxHex: response.TxHex, + }, nil +} + func callBuildTaggedTBTCSignerVersion() ([]byte, error) { result := C.tbtc_signer_version() return parseBuildTaggedTBTCSignerResult("Version", result) @@ -688,6 +892,18 @@ func callBuildTaggedTBTCSignerFinalizeSignRound( ) } +func callBuildTaggedTBTCSignerBuildTaprootTx( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "BuildTaprootTx", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_build_taproot_tx(requestPtr, requestLen) + }, + ) +} + func callBuildTaggedTBTCSignerOperation( operation string, requestPayload []byte, diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 2b118338ed..aaf8f4dc60 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -48,6 +48,28 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { t.Fatalf("unexpected bridge error: [%v]", err) } + _, err = engine.BuildTaprootTx( + "session-1", + []NativeTBTCSignerTxInput{ + {TxIDHex: "11", Vout: 0, ValueSats: 1}, + }, + []NativeTBTCSignerTxOutput{ + {ScriptPubKeyHex: "0014", ValueSats: 1}, + }, + nil, + ) + if err == nil { + t.Fatal("expected unavailable tbtc-signer build-tx bridge error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected native cryptography unavailable error: [%v], got [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + versionedEngine, ok := engine.(interface { Version() (string, error) }) @@ -582,3 +604,196 @@ func TestDecodeBuildTaggedTBTCSignerFinalizeSignRoundResponse(t *testing.T) { } } } + +func TestBuildTaggedTBTCSignerBuildTaprootTxRequestPayload(t *testing.T) { + scriptTreeHex := "deadbeef" + + payload, err := buildTaggedTBTCSignerBuildTaprootTxRequestPayload( + "session-buildtx-1", + []NativeTBTCSignerTxInput{ + { + TxIDHex: strings.Repeat("11", 32), + Vout: 3, + ValueSats: 1000, + }, + }, + []NativeTBTCSignerTxOutput{ + { + ScriptPubKeyHex: "0014deadbeef", + ValueSats: 900, + }, + }, + &scriptTreeHex, + ) + if err != nil { + t.Fatalf("unexpected payload build error: [%v]", err) + } + + var request buildTaggedTBTCSignerBuildTaprootTxRequest + if err := json.Unmarshal(payload, &request); err != nil { + t.Fatalf("cannot decode request payload: [%v]", err) + } + + if request.SessionID != "session-buildtx-1" { + t.Fatalf( + "unexpected session id\nexpected: [%v]\nactual: [%v]", + "session-buildtx-1", + request.SessionID, + ) + } + + if len(request.Inputs) != 1 { + t.Fatalf( + "unexpected input count\nexpected: [%v]\nactual: [%v]", + 1, + len(request.Inputs), + ) + } + + if request.Inputs[0].TxIDHex != strings.Repeat("11", 32) { + t.Fatalf( + "unexpected input txid\nexpected: [%v]\nactual: [%v]", + strings.Repeat("11", 32), + request.Inputs[0].TxIDHex, + ) + } + + if len(request.Outputs) != 1 { + t.Fatalf( + "unexpected output count\nexpected: [%v]\nactual: [%v]", + 1, + len(request.Outputs), + ) + } + + if request.Outputs[0].ScriptPubKeyHex != "0014deadbeef" { + t.Fatalf( + "unexpected output script pubkey\nexpected: [%v]\nactual: [%v]", + "0014deadbeef", + request.Outputs[0].ScriptPubKeyHex, + ) + } + + if request.ScriptTreeHex == nil || *request.ScriptTreeHex != scriptTreeHex { + t.Fatal("expected script tree hex to be present and preserved") + } +} + +func TestBuildTaggedTBTCSignerBuildTaprootTxRequestPayload_RejectsInvalidInput( + t *testing.T, +) { + scriptTreeHex := "" + + testCases := []struct { + name string + sessionID string + inputs []NativeTBTCSignerTxInput + outputs []NativeTBTCSignerTxOutput + scriptTreeHex *string + }{ + { + name: "empty session id", + sessionID: "", + inputs: []NativeTBTCSignerTxInput{ + {TxIDHex: strings.Repeat("11", 32), Vout: 0, ValueSats: 1}, + }, + outputs: []NativeTBTCSignerTxOutput{ + {ScriptPubKeyHex: "0014aa", ValueSats: 1}, + }, + }, + { + name: "empty inputs", + sessionID: "session-1", + inputs: nil, + outputs: []NativeTBTCSignerTxOutput{ + {ScriptPubKeyHex: "0014aa", ValueSats: 1}, + }, + }, + { + name: "empty outputs", + sessionID: "session-1", + inputs: []NativeTBTCSignerTxInput{ + {TxIDHex: strings.Repeat("11", 32), Vout: 0, ValueSats: 1}, + }, + outputs: nil, + }, + { + name: "input txid empty", + sessionID: "session-1", + inputs: []NativeTBTCSignerTxInput{ + {TxIDHex: "", Vout: 0, ValueSats: 1}, + }, + outputs: []NativeTBTCSignerTxOutput{ + {ScriptPubKeyHex: "0014aa", ValueSats: 1}, + }, + }, + { + name: "output script empty", + sessionID: "session-1", + inputs: []NativeTBTCSignerTxInput{ + {TxIDHex: strings.Repeat("11", 32), Vout: 0, ValueSats: 1}, + }, + outputs: []NativeTBTCSignerTxOutput{ + {ScriptPubKeyHex: "", ValueSats: 1}, + }, + }, + { + name: "script tree empty string", + sessionID: "session-1", + inputs: []NativeTBTCSignerTxInput{ + {TxIDHex: strings.Repeat("11", 32), Vout: 0, ValueSats: 1}, + }, + outputs: []NativeTBTCSignerTxOutput{ + {ScriptPubKeyHex: "0014aa", ValueSats: 1}, + }, + scriptTreeHex: &scriptTreeHex, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := buildTaggedTBTCSignerBuildTaprootTxRequestPayload( + tc.sessionID, + tc.inputs, + tc.outputs, + tc.scriptTreeHex, + ) + if err == nil { + t.Fatal("expected payload build error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected native cryptography unavailable error: [%v], got [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + }) + } +} + +func TestDecodeBuildTaggedTBTCSignerBuildTaprootTxResponse(t *testing.T) { + result, err := decodeBuildTaggedTBTCSignerBuildTaprootTxResponse( + []byte(`{"session_id":"session-buildtx-1","tx_hex":"deadbeef"}`), + ) + if err != nil { + t.Fatalf("unexpected decode error: [%v]", err) + } + + if result.SessionID != "session-buildtx-1" { + t.Fatalf( + "unexpected session id\nexpected: [%v]\nactual: [%v]", + "session-buildtx-1", + result.SessionID, + ) + } + + if result.TxHex != "deadbeef" { + t.Fatalf( + "unexpected tx hex\nexpected: [%v]\nactual: [%v]", + "deadbeef", + result.TxHex, + ) + } +} diff --git a/pkg/frost/signing/native_tbtc_signer_build_taproot_tx_frost_native.go b/pkg/frost/signing/native_tbtc_signer_build_taproot_tx_frost_native.go new file mode 100644 index 0000000000..e20207b8e3 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_build_taproot_tx_frost_native.go @@ -0,0 +1,36 @@ +//go:build frost_native + +package signing + +import "fmt" + +// BuildNativeTBTCSignerTaprootTx routes a BuildTaprootTx request through the +// currently-registered coarse tbtc-signer engine. +func BuildNativeTBTCSignerTaprootTx( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, +) (*NativeTBTCSignerTxResult, error) { + if sessionID == "" { + return nil, fmt.Errorf("session ID is empty") + } + + if len(inputs) == 0 { + return nil, fmt.Errorf("inputs are empty") + } + + if len(outputs) == 0 { + return nil, fmt.Errorf("outputs are empty") + } + + nativeEngine := currentNativeTBTCSignerEngine() + if nativeEngine == nil { + return nil, fmt.Errorf( + "%w: native tbtc-signer engine is unavailable", + ErrNativeCryptographyUnavailable, + ) + } + + return nativeEngine.BuildTaprootTx(sessionID, inputs, outputs, scriptTreeHex) +} diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go index 1ee7d20722..b19c88bf63 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -27,6 +27,28 @@ type NativeTBTCSignerRoundContribution struct { Data []byte `json:"data"` } +// NativeTBTCSignerTxInput describes an unsigned transaction input consumed by +// BuildTaprootTx. +type NativeTBTCSignerTxInput struct { + TxIDHex string `json:"txIDHex"` + Vout uint32 `json:"vout"` + ValueSats uint64 `json:"valueSats"` +} + +// NativeTBTCSignerTxOutput describes an unsigned transaction output consumed +// by BuildTaprootTx. +type NativeTBTCSignerTxOutput struct { + ScriptPubKeyHex string `json:"scriptPubKeyHex"` + ValueSats uint64 `json:"valueSats"` +} + +// NativeTBTCSignerTxResult captures unsigned transaction metadata returned by +// BuildTaprootTx. +type NativeTBTCSignerTxResult struct { + SessionID string `json:"sessionID"` + TxHex string `json:"txHex"` +} + // NativeTBTCSignerRoundState captures coarse session round metadata returned by // StartSignRound. type NativeTBTCSignerRoundState struct { @@ -57,6 +79,12 @@ type NativeTBTCSignerEngine interface { sessionID string, roundContributions []NativeTBTCSignerRoundContribution, ) ([]byte, error) + BuildTaprootTx( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, + ) (*NativeTBTCSignerTxResult, error) } var nativeTBTCSignerEngine NativeTBTCSignerEngine diff --git a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go index efc3f3660a..a0487c6f75 100644 --- a/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go @@ -36,6 +36,15 @@ func (mntse *mockNativeTBTCSignerEngine) FinalizeSignRound( return nil, fmt.Errorf("not implemented") } +func (mntse *mockNativeTBTCSignerEngine) BuildTaprootTx( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, +) (*NativeTBTCSignerTxResult, error) { + return nil, fmt.Errorf("not implemented") +} + func TestRegisterNativeTBTCSignerEngineRejectsNil(t *testing.T) { UnregisterNativeTBTCSignerEngine() t.Cleanup(UnregisterNativeTBTCSignerEngine) diff --git a/pkg/tbtc/native_tbtc_signer_build_taproot_tx_default.go b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_default.go new file mode 100644 index 0000000000..cf6334056e --- /dev/null +++ b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_default.go @@ -0,0 +1,13 @@ +//go:build !(frost_native && frost_tbtc_signer && cgo) + +package tbtc + +import "github.com/keep-network/keep-core/pkg/bitcoin" + +// buildTaprootTxViaNativeSigner is a no-op on builds that do not link the +// native tbtc-signer bridge. +func buildTaprootTxViaNativeSigner( + unsignedTx *bitcoin.TransactionBuilder, +) (string, error) { + return "", nil +} diff --git a/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go new file mode 100644 index 0000000000..82d03fa9de --- /dev/null +++ b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go @@ -0,0 +1,104 @@ +//go:build frost_native && frost_tbtc_signer && cgo + +package tbtc + +import ( + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + + "github.com/keep-network/keep-core/pkg/bitcoin" + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" +) + +func buildTaprootTxViaNativeSigner( + unsignedTx *bitcoin.TransactionBuilder, +) (string, error) { + if unsignedTx == nil { + return "", fmt.Errorf("unsigned transaction builder is nil") + } + + inputs, outputs, err := unsignedTx.UnsignedTransactionIO() + if err != nil { + return "", fmt.Errorf("cannot extract unsigned transaction I/O: [%w]", err) + } + + nativeInputs := make([]frostsigning.NativeTBTCSignerTxInput, 0, len(inputs)) + for _, input := range inputs { + nativeInputs = append( + nativeInputs, + frostsigning.NativeTBTCSignerTxInput{ + TxIDHex: input.TxIDHex, + Vout: input.Vout, + ValueSats: input.ValueSats, + }, + ) + } + + nativeOutputs := make([]frostsigning.NativeTBTCSignerTxOutput, 0, len(outputs)) + for _, output := range outputs { + nativeOutputs = append( + nativeOutputs, + frostsigning.NativeTBTCSignerTxOutput{ + ScriptPubKeyHex: output.ScriptPubKeyHex, + ValueSats: output.ValueSats, + }, + ) + } + + sessionID := buildTaprootTxSessionID(inputs, outputs) + + result, err := frostsigning.BuildNativeTBTCSignerTaprootTx( + sessionID, + nativeInputs, + nativeOutputs, + nil, + ) + if err != nil { + // Keep legacy fallback behavior when native tbtc-signer bridge is not + // linked/available for the running build. + if errors.Is(err, frostsigning.ErrNativeCryptographyUnavailable) { + return "", nil + } + + return "", err + } + + if result == nil { + return "", fmt.Errorf("native tbtc-signer returned nil BuildTaprootTx result") + } + + if result.SessionID != sessionID { + return "", fmt.Errorf( + "native tbtc-signer BuildTaprootTx returned unexpected session ID: [%v] != [%v]", + result.SessionID, + sessionID, + ) + } + + if result.TxHex == "" { + return "", fmt.Errorf("native tbtc-signer BuildTaprootTx returned empty tx hex") + } + + return result.TxHex, nil +} + +func buildTaprootTxSessionID( + inputs []bitcoin.UnsignedTransactionInput, + outputs []bitcoin.UnsignedTransactionOutput, +) string { + sessionPayload, err := json.Marshal(struct { + Inputs []bitcoin.UnsignedTransactionInput `json:"inputs"` + Outputs []bitcoin.UnsignedTransactionOutput `json:"outputs"` + }{ + Inputs: inputs, + Outputs: outputs, + }) + if err != nil { + return fmt.Sprintf("buildtx-fallback-%d-%d", len(inputs), len(outputs)) + } + + digest := sha256.Sum256(sessionPayload) + return fmt.Sprintf("buildtx-%x", digest[:]) +} diff --git a/pkg/tbtc/signing_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go index a1405b4d3c..cbd45b120d 100644 --- a/pkg/tbtc/signing_native_backend_frost_native_test.go +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -277,6 +277,15 @@ func (atntsfe *attemptTrackingNativeTBTCSignerEngineForTBTC) FinalizeSignRound( return []byte{0xaa}, nil } +func (atntsfe *attemptTrackingNativeTBTCSignerEngineForTBTC) BuildTaprootTx( + sessionID string, + inputs []frostsigning.NativeTBTCSignerTxInput, + outputs []frostsigning.NativeTBTCSignerTxOutput, + scriptTreeHex *string, +) (*frostsigning.NativeTBTCSignerTxResult, error) { + return nil, fmt.Errorf("not implemented") +} + func (atntsfe *attemptTrackingNativeTBTCSignerEngineForTBTC) uniqueStartCohortsByAttempt() map[uint][][]uint16 { atntsfe.mutex.Lock() defer atntsfe.mutex.Unlock() diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index dbb1543f09..97f0691466 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -296,6 +296,8 @@ type walletTransactionExecutor struct { waitForBlockFn waitForBlockFn } +var buildTaprootTxViaNativeSignerFn = buildTaprootTxViaNativeSigner + func newWalletTransactionExecutor( btcChain bitcoin.Chain, executingWallet wallet, @@ -319,6 +321,21 @@ func (wte *walletTransactionExecutor) signTransaction( signingStartBlock uint64, signingTimeoutBlock uint64, ) (*bitcoin.Transaction, error) { + nativeUnsignedTxHex, err := buildTaprootTxViaNativeSignerFn(unsignedTx) + if err != nil { + return nil, fmt.Errorf( + "error while building unsigned transaction with native tbtc-signer: [%v]", + err, + ) + } + + if nativeUnsignedTxHex != "" { + signTxLogger.Debugf( + "received unsigned transaction from native tbtc-signer BuildTaprootTx [txHexLen:%d]", + len(nativeUnsignedTxHex), + ) + } + signTxLogger.Infof("computing transaction's sig hashes") sigHashes, err := unsignedTx.ComputeSignatureHashes() diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go new file mode 100644 index 0000000000..320ca060a8 --- /dev/null +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -0,0 +1,35 @@ +package tbtc + +import ( + "errors" + "strings" + "testing" + + "github.com/keep-network/keep-core/pkg/bitcoin" +) + +func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( + t *testing.T, +) { + original := buildTaprootTxViaNativeSignerFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = original + }) + + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return "", errors.New("build tx failed") + } + + wte := &walletTransactionExecutor{} + + _, err := wte.signTransaction(nil, nil, 0, 0) + if err == nil { + t.Fatal("expected signTransaction error") + } + + if !strings.Contains(err.Error(), "native tbtc-signer") { + t.Fatalf("unexpected error: [%v]", err) + } +} From a11cbfa62721f54c5c8e02c2cd8c86d36dc32b19 Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 25 Feb 2026 09:20:23 -0600 Subject: [PATCH 76/96] Split native bridge operation errors and compare BuildTaprootTx IO --- pkg/bitcoin/transaction_builder.go | 2 + pkg/bitcoin/transaction_builder_test.go | 7 +- pkg/frost/signing/native_bridge.go | 6 + ...e_tbtc_signer_registration_frost_native.go | 42 +++- ...c_signer_registration_frost_native_test.go | 56 +++++ ...ild_taproot_tx_frost_native_tbtc_signer.go | 8 +- pkg/tbtc/wallet.go | 184 ++++++++++++++ ..._sign_transaction_build_taproot_tx_test.go | 235 ++++++++++++++++++ 8 files changed, 527 insertions(+), 13 deletions(-) diff --git a/pkg/bitcoin/transaction_builder.go b/pkg/bitcoin/transaction_builder.go index 852179b6ca..0d15bed5c2 100644 --- a/pkg/bitcoin/transaction_builder.go +++ b/pkg/bitcoin/transaction_builder.go @@ -350,6 +350,8 @@ func (tb *TransactionBuilder) UnsignedTransactionIO() ( inputs = append( inputs, UnsignedTransactionInput{ + // chainhash.Hash.String renders txid in standard Bitcoin display + // (RPC/explorer) byte order, i.e. reversed vs internal bytes. TxIDHex: input.PreviousOutPoint.Hash.String(), Vout: input.PreviousOutPoint.Index, ValueSats: uint64(value), diff --git a/pkg/bitcoin/transaction_builder_test.go b/pkg/bitcoin/transaction_builder_test.go index a911363cde..aaf36e10b8 100644 --- a/pkg/bitcoin/transaction_builder_test.go +++ b/pkg/bitcoin/transaction_builder_test.go @@ -222,8 +222,9 @@ func TestTransactionBuilder_UnsignedTransactionIO(t *testing.T) { var txHash chainhash.Hash for i := range txHash { - txHash[i] = 0x11 + txHash[i] = byte(i + 1) } + const expectedTxIDHex = "201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201" builder.internal.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, 7), nil, nil)) builder.sigHashArgs = append(builder.sigHashArgs, &inputSigHashArgs{value: 1234}) @@ -241,10 +242,10 @@ func TestTransactionBuilder_UnsignedTransactionIO(t *testing.T) { t.Fatalf("unexpected input count: [%d]", len(inputs)) } - if inputs[0].TxIDHex != txHash.String() { + if inputs[0].TxIDHex != expectedTxIDHex { t.Fatalf( "unexpected input txid\nexpected: [%v]\nactual: [%v]", - txHash.String(), + expectedTxIDHex, inputs[0].TxIDHex, ) } diff --git a/pkg/frost/signing/native_bridge.go b/pkg/frost/signing/native_bridge.go index 3f61a9f1b4..195369aed9 100644 --- a/pkg/frost/signing/native_bridge.go +++ b/pkg/frost/signing/native_bridge.go @@ -17,6 +17,12 @@ var ( ErrNativeCryptographyUnavailable = errors.New( "native FROST cryptographic execution is unavailable", ) + // ErrNativeBridgeOperationFailed indicates that native cryptographic + // execution is available but a bridge operation returned a non-success + // status. This error should not trigger availability fallback. + ErrNativeBridgeOperationFailed = errors.New( + "native FROST bridge operation failed", + ) ) // NativeExecutionBridge defines a native cryptographic execution entrypoint diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index bbec2a369f..05237ca3bc 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -343,6 +343,18 @@ func buildTaggedTBTCSignerOperationError( ) } +func buildTaggedTBTCSignerBridgeOperationError( + operation string, + message string, +) error { + return fmt.Errorf( + "%w: tbtc-signer bridge operation [%v] failed: [%s]", + ErrNativeBridgeOperationFailed, + operation, + message, + ) +} + func buildTaggedTBTCSignerRunDKGRequestPayload( sessionID string, participants []NativeTBTCSignerDKGParticipant, @@ -930,20 +942,15 @@ func parseBuildTaggedTBTCSignerResult( defer C.tbtc_signer_free_buffer(result.buffer.ptr, result.buffer.len) statusCode := int32(result.status_code) - if statusCode == buildTaggedTBTCSignerUnavailableStatusCode { - return nil, buildTaggedTBTCSignerUnavailableError(operation) - } var payload []byte if result.buffer.ptr != nil && result.buffer.len > 0 { payload = C.GoBytes(unsafe.Pointer(result.buffer.ptr), C.int(result.buffer.len)) } - if statusCode != 0 { - return nil, buildTaggedTBTCSignerOperationError( - operation, - buildTaggedTBTCSignerErrorMessage(payload), - ) + statusErr := buildTaggedTBTCSignerResultStatusError(operation, statusCode, payload) + if statusErr != nil { + return nil, statusErr } if len(payload) == 0 { @@ -956,6 +963,25 @@ func parseBuildTaggedTBTCSignerResult( return payload, nil } +func buildTaggedTBTCSignerResultStatusError( + operation string, + statusCode int32, + payload []byte, +) error { + if statusCode == buildTaggedTBTCSignerUnavailableStatusCode { + return buildTaggedTBTCSignerUnavailableError(operation) + } + + if statusCode != 0 { + return buildTaggedTBTCSignerBridgeOperationError( + operation, + buildTaggedTBTCSignerErrorMessage(payload), + ) + } + + return nil +} + func buildTaggedTBTCSignerErrorMessage(payload []byte) string { var errorResponse buildTaggedTBTCSignerErrorResponse if err := json.Unmarshal(payload, &errorResponse); err != nil { diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index aaf8f4dc60..39f2b0e224 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -91,6 +91,62 @@ func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { } } +func TestBuildTaggedTBTCSignerResultStatusError_Unavailable(t *testing.T) { + err := buildTaggedTBTCSignerResultStatusError( + "BuildTaprootTx", + buildTaggedTBTCSignerUnavailableStatusCode, + nil, + ) + if err == nil { + t.Fatal("expected unavailable error") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected native cryptography unavailable error: [%v], got [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if errors.Is(err, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "did not expect native bridge operation failed error: [%v]", + err, + ) + } +} + +func TestBuildTaggedTBTCSignerResultStatusError_BridgeOperationFailure(t *testing.T) { + err := buildTaggedTBTCSignerResultStatusError( + "BuildTaprootTx", + 2, + []byte(`{"code":"validation","message":"invalid input"}`), + ) + if err == nil { + t.Fatal("expected bridge operation failure error") + } + + if !errors.Is(err, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "expected native bridge operation failed error: [%v], got [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", + err, + ) + } + + if !strings.Contains(err.Error(), "validation: invalid input") { + t.Fatalf("unexpected bridge operation error: [%v]", err) + } +} + func TestBuildTaggedTBTCSignerRunDKGRequestPayload(t *testing.T) { payload, err := buildTaggedTBTCSignerRunDKGRequestPayload( "session-1", diff --git a/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go index 82d03fa9de..a615346856 100644 --- a/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go +++ b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go @@ -56,8 +56,10 @@ func buildTaprootTxViaNativeSigner( nil, ) if err != nil { - // Keep legacy fallback behavior when native tbtc-signer bridge is not - // linked/available for the running build. + // Keep legacy fallback behavior for the observational BuildTaprootTx + // phase when native bridge support is unavailable. + // Note that current bridge error mapping can also classify operational + // failures as unavailable; tighten this split before signing-substitution. if errors.Is(err, frostsigning.ErrNativeCryptographyUnavailable) { return "", nil } @@ -88,6 +90,8 @@ func buildTaprootTxSessionID( inputs []bitcoin.UnsignedTransactionInput, outputs []bitcoin.UnsignedTransactionOutput, ) string { + // Session ID is deterministically derived from Go-side transaction I/O using + // encoding/json. Rust currently treats this session_id as opaque. sessionPayload, err := json.Marshal(struct { Inputs []bitcoin.UnsignedTransactionInput `json:"inputs"` Outputs []bitcoin.UnsignedTransactionOutput `json:"outputs"` diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 97f0691466..cd209757b6 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -23,6 +23,11 @@ import ( "go.uber.org/zap" ) +type unsignedTransactionInputReference struct { + TxIDHex string + Vout uint32 +} + // WalletActionType represents actions types that can be performed by a wallet. type WalletActionType uint8 @@ -334,6 +339,21 @@ func (wte *walletTransactionExecutor) signTransaction( "received unsigned transaction from native tbtc-signer BuildTaprootTx [txHexLen:%d]", len(nativeUnsignedTxHex), ) + + expectedInputs, expectedOutputs, err := unsignedTx.UnsignedTransactionIO() + if err != nil { + signTxLogger.Warnf( + "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", + err, + ) + } else { + warnOnNativeUnsignedTransactionIODivergence( + signTxLogger, + nativeUnsignedTxHex, + expectedInputs, + expectedOutputs, + ) + } } signTxLogger.Infof("computing transaction's sig hashes") @@ -391,6 +411,170 @@ func (wte *walletTransactionExecutor) signTransaction( return tx, nil } +func warnOnNativeUnsignedTransactionIODivergence( + signTxLogger log.StandardLogger, + nativeUnsignedTxHex string, + expectedInputs []bitcoin.UnsignedTransactionInput, + expectedOutputs []bitcoin.UnsignedTransactionOutput, +) { + diverges, err := nativeUnsignedTransactionIODiverges( + nativeUnsignedTxHex, + expectedInputs, + expectedOutputs, + ) + if err != nil { + signTxLogger.Warnf( + "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", + err, + ) + return + } + + if diverges { + signTxLogger.Warnf( + "native BuildTaprootTx unsigned transaction I/O diverges from Go builder state", + ) + } +} + +func nativeUnsignedTransactionIODiverges( + nativeUnsignedTxHex string, + expectedInputs []bitcoin.UnsignedTransactionInput, + expectedOutputs []bitcoin.UnsignedTransactionOutput, +) (bool, error) { + nativeUnsignedTxBytes, err := hex.DecodeString(nativeUnsignedTxHex) + if err != nil { + return false, fmt.Errorf("cannot decode native tx hex: [%w]", err) + } + + nativeUnsignedTx := &bitcoin.Transaction{} + if err := nativeUnsignedTx.Deserialize(nativeUnsignedTxBytes); err != nil { + return false, fmt.Errorf("cannot deserialize native tx bytes: [%w]", err) + } + + actualInputReferences, actualOutputs, err := extractUnsignedTransactionIOFromTransaction( + nativeUnsignedTx, + ) + if err != nil { + return false, err + } + + expectedInputReferences := unsignedTransactionInputReferences(expectedInputs) + + return !unsignedTransactionInputReferencesEqual( + expectedInputReferences, + actualInputReferences, + ) || + !unsignedTransactionOutputsEqual( + expectedOutputs, + actualOutputs, + ), + nil +} + +func extractUnsignedTransactionIOFromTransaction( + transaction *bitcoin.Transaction, +) ( + []unsignedTransactionInputReference, + []bitcoin.UnsignedTransactionOutput, + error, +) { + inputReferences := make( + []unsignedTransactionInputReference, + 0, + len(transaction.Inputs), + ) + for i, input := range transaction.Inputs { + if input == nil { + return nil, nil, fmt.Errorf("transaction input [%d] is nil", i) + } + + if input.Outpoint == nil { + return nil, nil, fmt.Errorf("transaction input [%d] outpoint is nil", i) + } + + inputReferences = append( + inputReferences, + unsignedTransactionInputReference{ + TxIDHex: input.Outpoint.TransactionHash.Hex(bitcoin.ReversedByteOrder), + Vout: input.Outpoint.OutputIndex, + }, + ) + } + + outputs := make([]bitcoin.UnsignedTransactionOutput, 0, len(transaction.Outputs)) + for i, output := range transaction.Outputs { + if output == nil { + return nil, nil, fmt.Errorf("transaction output [%d] is nil", i) + } + + if output.Value < 0 { + return nil, nil, fmt.Errorf("transaction output [%d] value is negative", i) + } + + outputs = append( + outputs, + bitcoin.UnsignedTransactionOutput{ + ScriptPubKeyHex: hex.EncodeToString(output.PublicKeyScript), + ValueSats: uint64(output.Value), + }, + ) + } + + return inputReferences, outputs, nil +} + +func unsignedTransactionInputReferences( + inputs []bitcoin.UnsignedTransactionInput, +) []unsignedTransactionInputReference { + result := make([]unsignedTransactionInputReference, 0, len(inputs)) + for _, input := range inputs { + result = append( + result, + unsignedTransactionInputReference{ + TxIDHex: input.TxIDHex, + Vout: input.Vout, + }, + ) + } + + return result +} + +func unsignedTransactionInputReferencesEqual( + first []unsignedTransactionInputReference, + second []unsignedTransactionInputReference, +) bool { + if len(first) != len(second) { + return false + } + + for i := range first { + if first[i] != second[i] { + return false + } + } + + return true +} + +func unsignedTransactionOutputsEqual( + first []bitcoin.UnsignedTransactionOutput, + second []bitcoin.UnsignedTransactionOutput, +) bool { + if len(first) != len(second) { + return false + } + + for i := range first { + if first[i] != second[i] { + return false + } + } + + return true +} + // broadcastTransaction broadcasts a signed Bitcoin transaction until // the transaction lands in the Bitcoin mempool or the provided timeout // is hit, whichever comes first. diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index 320ca060a8..72964a09a3 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -1,7 +1,9 @@ package tbtc import ( + "encoding/hex" "errors" + "fmt" "strings" "testing" @@ -33,3 +35,236 @@ func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( t.Fatalf("unexpected error: [%v]", err) } } + +func TestNativeUnsignedTransactionIODiverges_MatchingIO(t *testing.T) { + txHashBytes := make([]byte, bitcoin.HashByteLength) + for i := range txHashBytes { + txHashBytes[i] = byte(i + 1) + } + + txHash, err := bitcoin.NewHash(txHashBytes, bitcoin.InternalByteOrder) + if err != nil { + t.Fatalf("cannot build tx hash: [%v]", err) + } + + scriptPubKey := mustDecodeHex(t, "0014deadbeef") + nativeTransaction := &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + } + + nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) + + expectedInputs := []bitcoin.UnsignedTransactionInput{ + { + TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), + Vout: 7, + ValueSats: 1234, + }, + } + expectedOutputs := []bitcoin.UnsignedTransactionOutput{ + { + ScriptPubKeyHex: "0014deadbeef", + ValueSats: 1000, + }, + } + + diverges, err := nativeUnsignedTransactionIODiverges( + nativeTxHex, + expectedInputs, + expectedOutputs, + ) + if err != nil { + t.Fatalf("unexpected comparison error: [%v]", err) + } + + if diverges { + t.Fatal("expected matching unsigned transaction I/O") + } +} + +func TestNativeUnsignedTransactionIODiverges_MismatchedIO(t *testing.T) { + txHashBytes := make([]byte, bitcoin.HashByteLength) + for i := range txHashBytes { + txHashBytes[i] = byte(i + 1) + } + + txHash, err := bitcoin.NewHash(txHashBytes, bitcoin.InternalByteOrder) + if err != nil { + t.Fatalf("cannot build tx hash: [%v]", err) + } + + scriptPubKey := mustDecodeHex(t, "0014deadbeef") + nativeTransaction := &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + } + + nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) + + expectedInputs := []bitcoin.UnsignedTransactionInput{ + { + TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), + Vout: 7, + ValueSats: 1234, + }, + } + expectedOutputs := []bitcoin.UnsignedTransactionOutput{ + { + ScriptPubKeyHex: "0014deadbeef", + ValueSats: 999, + }, + } + + diverges, err := nativeUnsignedTransactionIODiverges( + nativeTxHex, + expectedInputs, + expectedOutputs, + ) + if err != nil { + t.Fatalf("unexpected comparison error: [%v]", err) + } + + if !diverges { + t.Fatal("expected unsigned transaction I/O divergence") + } +} + +func TestWarnOnNativeUnsignedTransactionIODivergence_LogsWarning(t *testing.T) { + logger := &warningCaptureLogger{} + + txHashBytes := make([]byte, bitcoin.HashByteLength) + for i := range txHashBytes { + txHashBytes[i] = byte(i + 1) + } + + txHash, err := bitcoin.NewHash(txHashBytes, bitcoin.InternalByteOrder) + if err != nil { + t.Fatalf("cannot build tx hash: [%v]", err) + } + + scriptPubKey := mustDecodeHex(t, "0014deadbeef") + nativeTransaction := &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + } + + nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) + + warnOnNativeUnsignedTransactionIODivergence( + logger, + nativeTxHex, + []bitcoin.UnsignedTransactionInput{ + { + TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), + Vout: 7, + ValueSats: 1234, + }, + }, + []bitcoin.UnsignedTransactionOutput{ + { + ScriptPubKeyHex: "0014deadbeef", + ValueSats: 999, + }, + }, + ) + + if len(logger.warningMessages) != 1 { + t.Fatalf( + "unexpected warning message count\nexpected: [%v]\nactual: [%v]", + 1, + len(logger.warningMessages), + ) + } + + if !strings.Contains(logger.warningMessages[0], "diverges") { + t.Fatalf("unexpected warning message: [%v]", logger.warningMessages[0]) + } +} + +func mustDecodeHex(t *testing.T, value string) []byte { + result, err := hex.DecodeString(value) + if err != nil { + t.Fatalf("cannot decode hex: [%v]", err) + } + + return result +} + +type warningCaptureLogger struct { + warningMessages []string +} + +func (wcl *warningCaptureLogger) Debug(args ...interface{}) {} + +func (wcl *warningCaptureLogger) Debugf(format string, args ...interface{}) {} + +func (wcl *warningCaptureLogger) Error(args ...interface{}) {} + +func (wcl *warningCaptureLogger) Errorf(format string, args ...interface{}) {} + +func (wcl *warningCaptureLogger) Fatal(args ...interface{}) {} + +func (wcl *warningCaptureLogger) Fatalf(format string, args ...interface{}) {} + +func (wcl *warningCaptureLogger) Info(args ...interface{}) {} + +func (wcl *warningCaptureLogger) Infof(format string, args ...interface{}) {} + +func (wcl *warningCaptureLogger) Panic(args ...interface{}) {} + +func (wcl *warningCaptureLogger) Panicf(format string, args ...interface{}) {} + +func (wcl *warningCaptureLogger) Warn(args ...interface{}) {} + +func (wcl *warningCaptureLogger) Warnf(format string, args ...interface{}) { + wcl.warningMessages = append( + wcl.warningMessages, + fmt.Sprintf(format, args...), + ) +} From a8e151357df0013baa3f436aacc53af128abe911 Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 25 Feb 2026 13:40:29 -0600 Subject: [PATCH 77/96] Gate BuildTaprootTx signing substitution on verified native tx IO --- pkg/bitcoin/transaction_builder.go | 55 +++++ pkg/bitcoin/transaction_builder_test.go | 157 +++++++++++++++ pkg/tbtc/wallet.go | 124 ++++++++++-- ..._sign_transaction_build_taproot_tx_test.go | 188 +++++++++++++++++- 4 files changed, 510 insertions(+), 14 deletions(-) diff --git a/pkg/bitcoin/transaction_builder.go b/pkg/bitcoin/transaction_builder.go index 0d15bed5c2..1098e12b5d 100644 --- a/pkg/bitcoin/transaction_builder.go +++ b/pkg/bitcoin/transaction_builder.go @@ -310,6 +310,61 @@ func (tb *TransactionBuilder) TotalInputsValue() int64 { return totalInputsValue } +// ReplaceUnsignedTransaction replaces the internal unsigned transaction while +// preserving per-input sighash metadata collected during builder input setup. +func (tb *TransactionBuilder) ReplaceUnsignedTransaction( + transaction *Transaction, +) error { + if transaction == nil { + return fmt.Errorf("transaction is nil") + } + + if len(transaction.Inputs) != len(tb.sigHashArgs) { + return fmt.Errorf( + "input metadata mismatch: [%d] tx inputs, [%d] sighash args", + len(transaction.Inputs), + len(tb.sigHashArgs), + ) + } + + previousInputs := tb.internal.TxIn + + replacedInternal := newInternalTransaction() + replacedInternal.fromTransaction(transaction) + + for i := range replacedInternal.TxIn { + if i >= len(previousInputs) { + break + } + + previousInput := previousInputs[i] + replacedInput := replacedInternal.TxIn[i] + + if previousInput == nil || replacedInput == nil { + continue + } + + if tb.sigHashArgs[i].witness { + if len(replacedInput.Witness) == 0 && len(previousInput.Witness) == 1 { + redeemScript := append([]byte{}, previousInput.Witness[0]...) + replacedInput.Witness = wire.TxWitness{redeemScript} + } + } else { + if len(replacedInput.SignatureScript) == 0 && len(previousInput.SignatureScript) > 0 { + replacedInput.SignatureScript = append( + []byte{}, + previousInput.SignatureScript..., + ) + } + } + } + + tb.internal = replacedInternal + tb.sigHashes = nil + + return nil +} + // UnsignedTransactionInput carries canonical unsigned input metadata extracted // from the builder state. type UnsignedTransactionInput struct { diff --git a/pkg/bitcoin/transaction_builder_test.go b/pkg/bitcoin/transaction_builder_test.go index aaf36e10b8..8fcb810df1 100644 --- a/pkg/bitcoin/transaction_builder_test.go +++ b/pkg/bitcoin/transaction_builder_test.go @@ -217,6 +217,163 @@ func TestTransactionBuilder_AddOutput(t *testing.T) { assertInternalOutput(t, builder, 0, output) } +func TestTransactionBuilder_ReplaceUnsignedTransaction(t *testing.T) { + builder := NewTransactionBuilder(nil) + + var initialInputHash1 chainhash.Hash + var initialInputHash2 chainhash.Hash + initialInputHash1[0] = 0x11 + initialInputHash2[0] = 0x22 + + builder.internal.AddTxIn( + wire.NewTxIn( + wire.NewOutPoint(&initialInputHash1, 1), + []byte{0xde, 0xad}, + nil, + ), + ) + builder.internal.AddTxIn( + wire.NewTxIn( + wire.NewOutPoint(&initialInputHash2, 2), + nil, + [][]byte{{0xbe, 0xef}}, + ), + ) + builder.sigHashArgs = append( + builder.sigHashArgs, + &inputSigHashArgs{value: 111, scriptCode: []byte{0x51}, witness: false}, + &inputSigHashArgs{value: 222, scriptCode: []byte{0x52}, witness: true}, + ) + builder.sigHashes = []*big.Int{big.NewInt(1), big.NewInt(2)} + + var replacementInputHash1 chainhash.Hash + var replacementInputHash2 chainhash.Hash + replacementInputHash1[0] = 0x33 + replacementInputHash2[0] = 0x44 + + err := builder.ReplaceUnsignedTransaction( + &Transaction{ + Version: 2, + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash(replacementInputHash1), + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash(replacementInputHash2), + OutputIndex: 8, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*TransactionOutput{ + { + Value: 1000, + PublicKeyScript: hexToSlice(t, "0014deadbeef"), + }, + }, + Locktime: 0, + }, + ) + if err != nil { + t.Fatalf("unexpected replacement error: [%v]", err) + } + + if len(builder.sigHashes) != 0 { + t.Fatalf("expected sighashes reset after replacement: [%d]", len(builder.sigHashes)) + } + + // Preserve P2SH/P2WSH placeholder scripts needed for final signature + // application while replacing tx skeleton. + if !reflect.DeepEqual([]byte{0xde, 0xad}, builder.internal.TxIn[0].SignatureScript) { + t.Fatalf( + "unexpected preserved signature script\nexpected: [%x]\nactual: [%x]", + []byte{0xde, 0xad}, + builder.internal.TxIn[0].SignatureScript, + ) + } + + if len(builder.internal.TxIn[1].Witness) != 1 { + t.Fatalf("unexpected preserved witness length: [%d]", len(builder.internal.TxIn[1].Witness)) + } + + if !reflect.DeepEqual([]byte{0xbe, 0xef}, builder.internal.TxIn[1].Witness[0]) { + t.Fatalf( + "unexpected preserved witness script\nexpected: [%x]\nactual: [%x]", + []byte{0xbe, 0xef}, + builder.internal.TxIn[1].Witness[0], + ) + } + + inputs, outputs, err := builder.UnsignedTransactionIO() + if err != nil { + t.Fatalf("unexpected extraction error after replacement: [%v]", err) + } + + if len(inputs) != 2 { + t.Fatalf("unexpected input count after replacement: [%d]", len(inputs)) + } + + if inputs[0].TxIDHex != replacementInputHash1.String() || inputs[0].Vout != 7 { + t.Fatalf("unexpected first input after replacement: [%+v]", inputs[0]) + } + + if inputs[1].TxIDHex != replacementInputHash2.String() || inputs[1].Vout != 8 { + t.Fatalf("unexpected second input after replacement: [%+v]", inputs[1]) + } + + if len(outputs) != 1 { + t.Fatalf("unexpected output count after replacement: [%d]", len(outputs)) + } +} + +func TestTransactionBuilder_ReplaceUnsignedTransaction_RejectsInputMetadataMismatch( + t *testing.T, +) { + builder := NewTransactionBuilder(nil) + + var txHash chainhash.Hash + builder.internal.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, 0), nil, nil)) + builder.sigHashArgs = append(builder.sigHashArgs, &inputSigHashArgs{value: 1}) + + err := builder.ReplaceUnsignedTransaction( + &Transaction{ + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash(txHash), + OutputIndex: 0, + }, + }, + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash(txHash), + OutputIndex: 1, + }, + }, + }, + }, + ) + if err == nil { + t.Fatal("expected input metadata mismatch error") + } + + if !reflect.DeepEqual( + fmt.Sprintf( + "input metadata mismatch: [%d] tx inputs, [%d] sighash args", + 2, + 1, + ), + err.Error(), + ) { + t.Fatalf("unexpected error: [%v]", err) + } +} + func TestTransactionBuilder_UnsignedTransactionIO(t *testing.T) { builder := NewTransactionBuilder(nil) diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index cd209757b6..1e0b1ed60c 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -8,6 +8,8 @@ import ( "encoding/hex" "fmt" "math/big" + "os" + "strings" "sync" "time" @@ -302,6 +304,9 @@ type walletTransactionExecutor struct { } var buildTaprootTxViaNativeSignerFn = buildTaprootTxViaNativeSigner +var nativeBuildTaprootTxSigningSubstitutionEnabledFn = nativeBuildTaprootTxSigningSubstitutionEnabled + +const nativeBuildTaprootTxSigningSubstitutionEnvVar = "KEEP_CORE_NATIVE_BUILDTX_SIGNING_SUBSTITUTION" func newWalletTransactionExecutor( btcChain bitcoin.Chain, @@ -326,6 +331,8 @@ func (wte *walletTransactionExecutor) signTransaction( signingStartBlock uint64, signingTimeoutBlock uint64, ) (*bitcoin.Transaction, error) { + substitutionEnabled := nativeBuildTaprootTxSigningSubstitutionEnabledFn() + nativeUnsignedTxHex, err := buildTaprootTxViaNativeSignerFn(unsignedTx) if err != nil { return nil, fmt.Errorf( @@ -342,17 +349,44 @@ func (wte *walletTransactionExecutor) signTransaction( expectedInputs, expectedOutputs, err := unsignedTx.UnsignedTransactionIO() if err != nil { + if substitutionEnabled { + return nil, fmt.Errorf( + "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", + err, + ) + } + signTxLogger.Warnf( "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", err, ) } else { - warnOnNativeUnsignedTransactionIODivergence( + nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( signTxLogger, nativeUnsignedTxHex, expectedInputs, expectedOutputs, + substitutionEnabled, ) + if err != nil { + return nil, fmt.Errorf( + "cannot process native BuildTaprootTx unsigned transaction for signing: [%v]", + err, + ) + } + + if nativeUnsignedTx != nil { + if err := unsignedTx.ReplaceUnsignedTransaction(nativeUnsignedTx); err != nil { + return nil, fmt.Errorf( + "cannot substitute Go unsigned transaction with native BuildTaprootTx output: [%v]", + err, + ) + } + + signTxLogger.Infof( + "substituted Go unsigned transaction with native tbtc-signer BuildTaprootTx output", + ) + } } } @@ -411,47 +445,113 @@ func (wte *walletTransactionExecutor) signTransaction( return tx, nil } -func warnOnNativeUnsignedTransactionIODivergence( +func nativeBuildTaprootTxSigningSubstitutionEnabled() bool { + switch strings.ToLower( + strings.TrimSpace( + os.Getenv(nativeBuildTaprootTxSigningSubstitutionEnvVar), + ), + ) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} + +func evaluateNativeUnsignedTransactionForSigning( signTxLogger log.StandardLogger, nativeUnsignedTxHex string, expectedInputs []bitcoin.UnsignedTransactionInput, expectedOutputs []bitcoin.UnsignedTransactionOutput, -) { - diverges, err := nativeUnsignedTransactionIODiverges( - nativeUnsignedTxHex, + substitutionEnabled bool, +) (*bitcoin.Transaction, error) { + nativeUnsignedTx, err := decodeNativeUnsignedTransactionHex(nativeUnsignedTxHex) + if err != nil { + if substitutionEnabled { + return nil, err + } + + signTxLogger.Warnf( + "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", + err, + ) + return nil, nil + } + + diverges, err := nativeUnsignedTransactionIODivergesFromTransaction( + nativeUnsignedTx, expectedInputs, expectedOutputs, ) if err != nil { + if substitutionEnabled { + return nil, err + } + signTxLogger.Warnf( "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", err, ) - return + return nil, nil } if diverges { + if substitutionEnabled { + return nil, fmt.Errorf( + "native BuildTaprootTx unsigned transaction I/O diverges from Go builder state", + ) + } + signTxLogger.Warnf( "native BuildTaprootTx unsigned transaction I/O diverges from Go builder state", ) } + + if substitutionEnabled { + return nativeUnsignedTx, nil + } + + return nil, nil } -func nativeUnsignedTransactionIODiverges( +func decodeNativeUnsignedTransactionHex( nativeUnsignedTxHex string, - expectedInputs []bitcoin.UnsignedTransactionInput, - expectedOutputs []bitcoin.UnsignedTransactionOutput, -) (bool, error) { +) (*bitcoin.Transaction, error) { nativeUnsignedTxBytes, err := hex.DecodeString(nativeUnsignedTxHex) if err != nil { - return false, fmt.Errorf("cannot decode native tx hex: [%w]", err) + return nil, fmt.Errorf("cannot decode native tx hex: [%w]", err) } nativeUnsignedTx := &bitcoin.Transaction{} if err := nativeUnsignedTx.Deserialize(nativeUnsignedTxBytes); err != nil { - return false, fmt.Errorf("cannot deserialize native tx bytes: [%w]", err) + return nil, fmt.Errorf("cannot deserialize native tx bytes: [%w]", err) + } + + return nativeUnsignedTx, nil +} + +func nativeUnsignedTransactionIODiverges( + nativeUnsignedTxHex string, + expectedInputs []bitcoin.UnsignedTransactionInput, + expectedOutputs []bitcoin.UnsignedTransactionOutput, +) (bool, error) { + nativeUnsignedTx, err := decodeNativeUnsignedTransactionHex(nativeUnsignedTxHex) + if err != nil { + return false, err } + return nativeUnsignedTransactionIODivergesFromTransaction( + nativeUnsignedTx, + expectedInputs, + expectedOutputs, + ) +} + +func nativeUnsignedTransactionIODivergesFromTransaction( + nativeUnsignedTx *bitcoin.Transaction, + expectedInputs []bitcoin.UnsignedTransactionInput, + expectedOutputs []bitcoin.UnsignedTransactionOutput, +) (bool, error) { actualInputReferences, actualOutputs, err := extractUnsignedTransactionIOFromTransaction( nativeUnsignedTx, ) diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index 72964a09a3..76942ecc8a 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -160,7 +160,9 @@ func TestNativeUnsignedTransactionIODiverges_MismatchedIO(t *testing.T) { } } -func TestWarnOnNativeUnsignedTransactionIODivergence_LogsWarning(t *testing.T) { +func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarning( + t *testing.T, +) { logger := &warningCaptureLogger{} txHashBytes := make([]byte, bitcoin.HashByteLength) @@ -196,7 +198,7 @@ func TestWarnOnNativeUnsignedTransactionIODivergence_LogsWarning(t *testing.T) { nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) - warnOnNativeUnsignedTransactionIODivergence( + nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( logger, nativeTxHex, []bitcoin.UnsignedTransactionInput{ @@ -212,7 +214,15 @@ func TestWarnOnNativeUnsignedTransactionIODivergence_LogsWarning(t *testing.T) { ValueSats: 999, }, }, + false, ) + if err != nil { + t.Fatalf("unexpected evaluation error: [%v]", err) + } + + if nativeUnsignedTx != nil { + t.Fatal("did not expect native transaction substitution in observational mode") + } if len(logger.warningMessages) != 1 { t.Fatalf( @@ -227,6 +237,180 @@ func TestWarnOnNativeUnsignedTransactionIODivergence_LogsWarning(t *testing.T) { } } +func TestEvaluateNativeUnsignedTransactionForSigning_SubstitutionModeRejectsDivergence( + t *testing.T, +) { + logger := &warningCaptureLogger{} + + txHashBytes := make([]byte, bitcoin.HashByteLength) + for i := range txHashBytes { + txHashBytes[i] = byte(i + 1) + } + + txHash, err := bitcoin.NewHash(txHashBytes, bitcoin.InternalByteOrder) + if err != nil { + t.Fatalf("cannot build tx hash: [%v]", err) + } + + scriptPubKey := mustDecodeHex(t, "0014deadbeef") + nativeTransaction := &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + } + + nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) + + nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( + logger, + nativeTxHex, + []bitcoin.UnsignedTransactionInput{ + { + TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), + Vout: 7, + ValueSats: 1234, + }, + }, + []bitcoin.UnsignedTransactionOutput{ + { + ScriptPubKeyHex: "0014deadbeef", + ValueSats: 999, + }, + }, + true, + ) + if err == nil { + t.Fatal("expected substitution-mode divergence error") + } + + if !strings.Contains(err.Error(), "diverges") { + t.Fatalf("unexpected substitution-mode error: [%v]", err) + } + + if nativeUnsignedTx != nil { + t.Fatal("did not expect native transaction on divergence") + } + + if len(logger.warningMessages) != 0 { + t.Fatalf("unexpected warnings in substitution mode: [%v]", logger.warningMessages) + } +} + +func TestEvaluateNativeUnsignedTransactionForSigning_SubstitutionModeAcceptsMatchingIO( + t *testing.T, +) { + logger := &warningCaptureLogger{} + + txHashBytes := make([]byte, bitcoin.HashByteLength) + for i := range txHashBytes { + txHashBytes[i] = byte(i + 1) + } + + txHash, err := bitcoin.NewHash(txHashBytes, bitcoin.InternalByteOrder) + if err != nil { + t.Fatalf("cannot build tx hash: [%v]", err) + } + + scriptPubKey := mustDecodeHex(t, "0014deadbeef") + nativeTransaction := &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + } + + nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) + + nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( + logger, + nativeTxHex, + []bitcoin.UnsignedTransactionInput{ + { + TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), + Vout: 7, + ValueSats: 1234, + }, + }, + []bitcoin.UnsignedTransactionOutput{ + { + ScriptPubKeyHex: "0014deadbeef", + ValueSats: 1000, + }, + }, + true, + ) + if err != nil { + t.Fatalf("unexpected substitution-mode evaluation error: [%v]", err) + } + + if nativeUnsignedTx == nil { + t.Fatal("expected native transaction substitution candidate") + } + + if len(logger.warningMessages) != 0 { + t.Fatalf("unexpected warnings in substitution mode: [%v]", logger.warningMessages) + } +} + +func TestNativeBuildTaprootTxSigningSubstitutionEnabled(t *testing.T) { + testCases := []struct { + name string + envValue string + expected bool + }{ + {name: "unset", envValue: "", expected: false}, + {name: "true", envValue: "true", expected: true}, + {name: "TRUE", envValue: "TRUE", expected: true}, + {name: "one", envValue: "1", expected: true}, + {name: "yes", envValue: "yes", expected: true}, + {name: "on", envValue: "on", expected: true}, + {name: "false", envValue: "false", expected: false}, + {name: "zero", envValue: "0", expected: false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv(nativeBuildTaprootTxSigningSubstitutionEnvVar, tc.envValue) + + actual := nativeBuildTaprootTxSigningSubstitutionEnabled() + if actual != tc.expected { + t.Fatalf( + "unexpected flag state\nexpected: [%v]\nactual: [%v]", + tc.expected, + actual, + ) + } + }) + } +} + func mustDecodeHex(t *testing.T, value string) []byte { result, err := hex.DecodeString(value) if err != nil { From bd9efb6485173a268fcacacc4d8d3068ebc1e2f0 Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 25 Feb 2026 14:11:25 -0600 Subject: [PATCH 78/96] Add end-to-end tests for BuildTaprootTx substitution path --- ..._sign_transaction_build_taproot_tx_test.go | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index 76942ecc8a..79448b491b 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -1,13 +1,20 @@ package tbtc import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/rand" "encoding/hex" "errors" "fmt" + "math/big" "strings" "testing" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/tecdsa" ) func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( @@ -411,6 +418,289 @@ func TestNativeBuildTaprootTxSigningSubstitutionEnabled(t *testing.T) { } } +func TestWalletTransactionExecutor_SignTransaction_SubstitutesNativeUnsignedTransactionWhenGateEnabled( + t *testing.T, +) { + privateKey, unsignedTx, nativeUnsignedTxHex, nativeUnsignedTx := buildTaprootTxSubstitutionFixture(t) + + originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn + originalSigningSubstitutionEnabledFn := nativeBuildTaprootTxSigningSubstitutionEnabledFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = originalBuildTaprootTxViaNativeSignerFn + nativeBuildTaprootTxSigningSubstitutionEnabledFn = originalSigningSubstitutionEnabledFn + }) + + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return nativeUnsignedTxHex, nil + } + nativeBuildTaprootTxSigningSubstitutionEnabledFn = func() bool { + return true + } + + wte := &walletTransactionExecutor{ + executingWallet: wallet{ + publicKey: &privateKey.PublicKey, + }, + signingExecutor: &deterministicECDSASigningExecutorForBuildTaprootTxSubstitution{ + privateKey: privateKey, + }, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + + logger := &warningCaptureLogger{} + + tx, err := wte.signTransaction(logger, unsignedTx, 0, 0) + if err != nil { + t.Fatalf("unexpected signTransaction error: [%v]", err) + } + + if tx.Version != nativeUnsignedTx.Version { + t.Fatalf( + "unexpected substituted transaction version\nexpected: [%v]\nactual: [%v]", + nativeUnsignedTx.Version, + tx.Version, + ) + } + + if tx.Locktime != nativeUnsignedTx.Locktime { + t.Fatalf( + "unexpected substituted transaction locktime\nexpected: [%v]\nactual: [%v]", + nativeUnsignedTx.Locktime, + tx.Locktime, + ) + } + + if len(tx.Inputs) != len(nativeUnsignedTx.Inputs) { + t.Fatalf( + "unexpected substituted input count\nexpected: [%v]\nactual: [%v]", + len(nativeUnsignedTx.Inputs), + len(tx.Inputs), + ) + } + + if tx.Inputs[0].Outpoint.TransactionHash != nativeUnsignedTx.Inputs[0].Outpoint.TransactionHash { + t.Fatalf( + "unexpected substituted input txid\nexpected: [%v]\nactual: [%v]", + nativeUnsignedTx.Inputs[0].Outpoint.TransactionHash, + tx.Inputs[0].Outpoint.TransactionHash, + ) + } + + if tx.Inputs[0].Outpoint.OutputIndex != nativeUnsignedTx.Inputs[0].Outpoint.OutputIndex { + t.Fatalf( + "unexpected substituted input vout\nexpected: [%v]\nactual: [%v]", + nativeUnsignedTx.Inputs[0].Outpoint.OutputIndex, + tx.Inputs[0].Outpoint.OutputIndex, + ) + } + + if tx.Inputs[0].Sequence != nativeUnsignedTx.Inputs[0].Sequence { + t.Fatalf( + "unexpected substituted input sequence\nexpected: [%v]\nactual: [%v]", + nativeUnsignedTx.Inputs[0].Sequence, + tx.Inputs[0].Sequence, + ) + } + + if len(tx.Inputs[0].SignatureScript) == 0 { + t.Fatal("expected signature script to be populated after signing") + } + + if len(tx.Outputs) != len(nativeUnsignedTx.Outputs) { + t.Fatalf( + "unexpected substituted output count\nexpected: [%v]\nactual: [%v]", + len(nativeUnsignedTx.Outputs), + len(tx.Outputs), + ) + } + + if tx.Outputs[0].Value != nativeUnsignedTx.Outputs[0].Value { + t.Fatalf( + "unexpected substituted output value\nexpected: [%v]\nactual: [%v]", + nativeUnsignedTx.Outputs[0].Value, + tx.Outputs[0].Value, + ) + } + + if !bytes.Equal( + tx.Outputs[0].PublicKeyScript, + nativeUnsignedTx.Outputs[0].PublicKeyScript, + ) { + t.Fatalf( + "unexpected substituted output script\nexpected: [%x]\nactual: [%x]", + nativeUnsignedTx.Outputs[0].PublicKeyScript, + tx.Outputs[0].PublicKeyScript, + ) + } + + if len(logger.warningMessages) != 0 { + t.Fatalf("unexpected warning logs: [%v]", logger.warningMessages) + } +} + +func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisabled( + t *testing.T, +) { + privateKey, unsignedTx, nativeUnsignedTxHex, nativeUnsignedTx := buildTaprootTxSubstitutionFixture(t) + + originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn + originalSigningSubstitutionEnabledFn := nativeBuildTaprootTxSigningSubstitutionEnabledFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = originalBuildTaprootTxViaNativeSignerFn + nativeBuildTaprootTxSigningSubstitutionEnabledFn = originalSigningSubstitutionEnabledFn + }) + + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return nativeUnsignedTxHex, nil + } + nativeBuildTaprootTxSigningSubstitutionEnabledFn = func() bool { + return false + } + + wte := &walletTransactionExecutor{ + executingWallet: wallet{ + publicKey: &privateKey.PublicKey, + }, + signingExecutor: &deterministicECDSASigningExecutorForBuildTaprootTxSubstitution{ + privateKey: privateKey, + }, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + + logger := &warningCaptureLogger{} + + tx, err := wte.signTransaction(logger, unsignedTx, 0, 0) + if err != nil { + t.Fatalf("unexpected signTransaction error: [%v]", err) + } + + if tx.Version == nativeUnsignedTx.Version { + t.Fatalf( + "did not expect transaction version substitution when gate disabled: [%v]", + tx.Version, + ) + } + + if tx.Locktime == nativeUnsignedTx.Locktime { + t.Fatalf( + "did not expect transaction locktime substitution when gate disabled: [%v]", + tx.Locktime, + ) + } + + if tx.Inputs[0].Sequence == nativeUnsignedTx.Inputs[0].Sequence { + t.Fatalf( + "did not expect input sequence substitution when gate disabled: [%v]", + tx.Inputs[0].Sequence, + ) + } + + if len(logger.warningMessages) != 0 { + t.Fatalf("unexpected warning logs: [%v]", logger.warningMessages) + } +} + +func buildTaprootTxSubstitutionFixture( + t *testing.T, +) ( + *ecdsa.PrivateKey, + *bitcoin.TransactionBuilder, + string, + *bitcoin.Transaction, +) { + privateKey := &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: tecdsa.Curve, + }, + D: big.NewInt(111), + } + privateKey.PublicKey.X, privateKey.PublicKey.Y = tecdsa.Curve.ScalarBaseMult( + privateKey.D.Bytes(), + ) + + pubKeyHash := [20]byte{} + for i := range pubKeyHash { + pubKeyHash[i] = byte(i + 1) + } + + lockingScript, err := bitcoin.PayToPublicKeyHash(pubKeyHash) + if err != nil { + t.Fatalf("cannot create locking script: [%v]", err) + } + + localBitcoinChain := newLocalBitcoinChain() + + fundingTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{}, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 10000, + PublicKeyScript: lockingScript, + }, + }, + Locktime: 0, + } + + if err := localBitcoinChain.BroadcastTransaction(fundingTransaction); err != nil { + t.Fatalf("cannot broadcast funding transaction: [%v]", err) + } + + unsignedTx := bitcoin.NewTransactionBuilder(localBitcoinChain) + if err := unsignedTx.AddPublicKeyHashInput( + &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTransaction.Hash(), + OutputIndex: 0, + }, + Value: 10000, + }, + ); err != nil { + t.Fatalf("cannot add unsigned input: [%v]", err) + } + + replacementOutputScript := mustDecodeHex(t, "0014deadbeef") + unsignedTx.AddOutput( + &bitcoin.TransactionOutput{ + Value: 9000, + PublicKeyScript: replacementOutputScript, + }, + ) + + nativeUnsignedTx := &bitcoin.Transaction{ + Version: 3, + Locktime: 123, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTransaction.Hash(), + OutputIndex: 0, + }, + Sequence: 0xfffffffd, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 9000, + PublicKeyScript: replacementOutputScript, + }, + }, + } + + return privateKey, + unsignedTx, + hex.EncodeToString(nativeUnsignedTx.Serialize(bitcoin.Standard)), + nativeUnsignedTx +} + func mustDecodeHex(t *testing.T, value string) []byte { result, err := hex.DecodeString(value) if err != nil { @@ -452,3 +742,36 @@ func (wcl *warningCaptureLogger) Warnf(format string, args ...interface{}) { fmt.Sprintf(format, args...), ) } + +type deterministicECDSASigningExecutorForBuildTaprootTxSubstitution struct { + privateKey *ecdsa.PrivateKey +} + +func (desefbts *deterministicECDSASigningExecutorForBuildTaprootTxSubstitution) signBatch( + ctx context.Context, + messages []*big.Int, + startBlock uint64, +) ([]*frost.Signature, error) { + signatures := make([]*frost.Signature, 0, len(messages)) + + for _, message := range messages { + r, s, err := ecdsa.Sign( + rand.Reader, + desefbts.privateKey, + message.Bytes(), + ) + if err != nil { + return nil, err + } + + signature := &frost.Signature{} + rBytes := r.Bytes() + copy(signature.R[len(signature.R)-len(rBytes):], rBytes) + sBytes := s.Bytes() + copy(signature.S[len(signature.S)-len(sBytes):], sBytes) + + signatures = append(signatures, signature) + } + + return signatures, nil +} From 9a708a6741fdf9876e90a662778e82c49b5afd4e Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 25 Feb 2026 15:05:31 -0600 Subject: [PATCH 79/96] Harden BuildTaprootTx substitution E2E coverage --- ..._sign_transaction_build_taproot_tx_test.go | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index 79448b491b..880ffaf429 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -582,6 +582,13 @@ func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisa t.Fatalf("unexpected signTransaction error: [%v]", err) } + if tx.Version != 1 { + t.Fatalf( + "unexpected non-substituted transaction version\nexpected: [1]\nactual: [%v]", + tx.Version, + ) + } + if tx.Version == nativeUnsignedTx.Version { t.Fatalf( "did not expect transaction version substitution when gate disabled: [%v]", @@ -589,6 +596,13 @@ func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisa ) } + if tx.Locktime != 0 { + t.Fatalf( + "unexpected non-substituted transaction locktime\nexpected: [0]\nactual: [%v]", + tx.Locktime, + ) + } + if tx.Locktime == nativeUnsignedTx.Locktime { t.Fatalf( "did not expect transaction locktime substitution when gate disabled: [%v]", @@ -596,6 +610,13 @@ func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisa ) } + if tx.Inputs[0].Sequence != 0xffffffff { + t.Fatalf( + "unexpected non-substituted input sequence\nexpected: [4294967295]\nactual: [%v]", + tx.Inputs[0].Sequence, + ) + } + if tx.Inputs[0].Sequence == nativeUnsignedTx.Inputs[0].Sequence { t.Fatalf( "did not expect input sequence substitution when gate disabled: [%v]", @@ -608,6 +629,78 @@ func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisa } } +func TestWalletTransactionExecutor_SignTransaction_RejectsNativeUnsignedTransactionDivergenceWhenGateEnabled( + t *testing.T, +) { + privateKey, unsignedTx, _, nativeUnsignedTx := buildTaprootTxSubstitutionFixture(t) + + divergingNativeUnsignedTx := *nativeUnsignedTx + divergingOutputs := make( + []*bitcoin.TransactionOutput, + len(nativeUnsignedTx.Outputs), + ) + for i, output := range nativeUnsignedTx.Outputs { + if output == nil { + t.Fatalf("native fixture output [%d] is nil", i) + } + + clonedOutput := *output + divergingOutputs[i] = &clonedOutput + } + divergingNativeUnsignedTx.Outputs = divergingOutputs + divergingNativeUnsignedTx.Outputs[0].Value = nativeUnsignedTx.Outputs[0].Value - 1 + divergingNativeUnsignedTxHex := hex.EncodeToString( + divergingNativeUnsignedTx.Serialize(bitcoin.Standard), + ) + + originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn + originalSigningSubstitutionEnabledFn := nativeBuildTaprootTxSigningSubstitutionEnabledFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = originalBuildTaprootTxViaNativeSignerFn + nativeBuildTaprootTxSigningSubstitutionEnabledFn = originalSigningSubstitutionEnabledFn + }) + + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return divergingNativeUnsignedTxHex, nil + } + nativeBuildTaprootTxSigningSubstitutionEnabledFn = func() bool { + return true + } + + wte := &walletTransactionExecutor{ + executingWallet: wallet{ + publicKey: &privateKey.PublicKey, + }, + signingExecutor: &deterministicECDSASigningExecutorForBuildTaprootTxSubstitution{ + privateKey: privateKey, + }, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + + logger := &warningCaptureLogger{} + + tx, err := wte.signTransaction(logger, unsignedTx, 0, 0) + if err == nil { + t.Fatal("expected signTransaction divergence error") + } + + if tx != nil { + t.Fatal("expected no signed transaction on substitution divergence") + } + + if !strings.Contains(err.Error(), "diverges") { + t.Fatalf("unexpected signTransaction divergence error: [%v]", err) + } + + if len(logger.warningMessages) != 0 { + t.Fatalf("unexpected warning logs in substitution mode: [%v]", logger.warningMessages) + } +} + func buildTaprootTxSubstitutionFixture( t *testing.T, ) ( From 3743421b8424a6cd73436a9900612d11b6892e3a Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 25 Feb 2026 15:52:40 -0600 Subject: [PATCH 80/96] Harden native BuildTaprootTx structural divergence checks --- pkg/bitcoin/transaction_builder.go | 5 + pkg/tbtc/wallet.go | 165 +++++++---- ..._sign_transaction_build_taproot_tx_test.go | 279 ++++++++++++++---- 3 files changed, 339 insertions(+), 110 deletions(-) diff --git a/pkg/bitcoin/transaction_builder.go b/pkg/bitcoin/transaction_builder.go index 1098e12b5d..95e5256ec5 100644 --- a/pkg/bitcoin/transaction_builder.go +++ b/pkg/bitcoin/transaction_builder.go @@ -365,6 +365,11 @@ func (tb *TransactionBuilder) ReplaceUnsignedTransaction( return nil } +// UnsignedTransaction returns the current unsigned transaction builder state. +func (tb *TransactionBuilder) UnsignedTransaction() *Transaction { + return tb.internal.toTransaction() +} + // UnsignedTransactionInput carries canonical unsigned input metadata extracted // from the builder state. type UnsignedTransactionInput struct { diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 1e0b1ed60c..15e53b1057 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -347,46 +347,30 @@ func (wte *walletTransactionExecutor) signTransaction( len(nativeUnsignedTxHex), ) - expectedInputs, expectedOutputs, err := unsignedTx.UnsignedTransactionIO() + nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( + signTxLogger, + nativeUnsignedTxHex, + unsignedTx.UnsignedTransaction(), + substitutionEnabled, + ) if err != nil { - if substitutionEnabled { - return nil, fmt.Errorf( - "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", - err, - ) - } - - signTxLogger.Warnf( - "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", + return nil, fmt.Errorf( + "cannot process native BuildTaprootTx unsigned transaction for signing: [%v]", err, ) - } else { - nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( - signTxLogger, - nativeUnsignedTxHex, - expectedInputs, - expectedOutputs, - substitutionEnabled, - ) - if err != nil { + } + + if nativeUnsignedTx != nil { + if err := unsignedTx.ReplaceUnsignedTransaction(nativeUnsignedTx); err != nil { return nil, fmt.Errorf( - "cannot process native BuildTaprootTx unsigned transaction for signing: [%v]", + "cannot substitute Go unsigned transaction with native BuildTaprootTx output: [%v]", err, ) } - if nativeUnsignedTx != nil { - if err := unsignedTx.ReplaceUnsignedTransaction(nativeUnsignedTx); err != nil { - return nil, fmt.Errorf( - "cannot substitute Go unsigned transaction with native BuildTaprootTx output: [%v]", - err, - ) - } - - signTxLogger.Infof( - "substituted Go unsigned transaction with native tbtc-signer BuildTaprootTx output", - ) - } + signTxLogger.Infof( + "substituted Go unsigned transaction with native tbtc-signer BuildTaprootTx output", + ) } } @@ -461,8 +445,7 @@ func nativeBuildTaprootTxSigningSubstitutionEnabled() bool { func evaluateNativeUnsignedTransactionForSigning( signTxLogger log.StandardLogger, nativeUnsignedTxHex string, - expectedInputs []bitcoin.UnsignedTransactionInput, - expectedOutputs []bitcoin.UnsignedTransactionOutput, + expectedTransaction *bitcoin.Transaction, substitutionEnabled bool, ) (*bitcoin.Transaction, error) { nativeUnsignedTx, err := decodeNativeUnsignedTransactionHex(nativeUnsignedTxHex) @@ -472,16 +455,15 @@ func evaluateNativeUnsignedTransactionForSigning( } signTxLogger.Warnf( - "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", + "cannot compare native BuildTaprootTx unsigned transaction with Go builder state: [%v]", err, ) return nil, nil } - diverges, err := nativeUnsignedTransactionIODivergesFromTransaction( + diverges, err := nativeUnsignedTransactionDivergesFromTransaction( nativeUnsignedTx, - expectedInputs, - expectedOutputs, + expectedTransaction, ) if err != nil { if substitutionEnabled { @@ -489,7 +471,7 @@ func evaluateNativeUnsignedTransactionForSigning( } signTxLogger.Warnf( - "cannot compare native BuildTaprootTx unsigned transaction I/O with Go builder state: [%v]", + "cannot compare native BuildTaprootTx unsigned transaction with Go builder state: [%v]", err, ) return nil, nil @@ -498,12 +480,12 @@ func evaluateNativeUnsignedTransactionForSigning( if diverges { if substitutionEnabled { return nil, fmt.Errorf( - "native BuildTaprootTx unsigned transaction I/O diverges from Go builder state", + "native BuildTaprootTx unsigned transaction diverges from Go builder state", ) } signTxLogger.Warnf( - "native BuildTaprootTx unsigned transaction I/O diverges from Go builder state", + "native BuildTaprootTx unsigned transaction diverges from Go builder state", ) } @@ -547,6 +529,37 @@ func nativeUnsignedTransactionIODiverges( ) } +func nativeUnsignedTransactionDivergesFromTransaction( + nativeUnsignedTx *bitcoin.Transaction, + expectedTransaction *bitcoin.Transaction, +) (bool, error) { + actualShape, err := extractUnsignedTransactionShapeFromTransaction(nativeUnsignedTx) + if err != nil { + return false, err + } + + expectedShape, err := extractUnsignedTransactionShapeFromTransaction(expectedTransaction) + if err != nil { + return false, err + } + + return actualShape.Version != expectedShape.Version || + actualShape.Locktime != expectedShape.Locktime || + !unsignedTransactionInputReferencesEqual( + actualShape.InputReferences, + expectedShape.InputReferences, + ) || + !unsignedTransactionInputSequencesEqual( + actualShape.InputSequences, + expectedShape.InputSequences, + ) || + !unsignedTransactionOutputsEqual( + actualShape.Outputs, + expectedShape.Outputs, + ), + nil +} + func nativeUnsignedTransactionIODivergesFromTransaction( nativeUnsignedTx *bitcoin.Transaction, expectedInputs []bitcoin.UnsignedTransactionInput, @@ -572,25 +585,34 @@ func nativeUnsignedTransactionIODivergesFromTransaction( nil } -func extractUnsignedTransactionIOFromTransaction( +type unsignedTransactionShape struct { + Version int32 + Locktime uint32 + InputReferences []unsignedTransactionInputReference + InputSequences []uint32 + Outputs []bitcoin.UnsignedTransactionOutput +} + +func extractUnsignedTransactionShapeFromTransaction( transaction *bitcoin.Transaction, -) ( - []unsignedTransactionInputReference, - []bitcoin.UnsignedTransactionOutput, - error, -) { +) (*unsignedTransactionShape, error) { + if transaction == nil { + return nil, fmt.Errorf("transaction is nil") + } + inputReferences := make( []unsignedTransactionInputReference, 0, len(transaction.Inputs), ) + inputSequences := make([]uint32, 0, len(transaction.Inputs)) for i, input := range transaction.Inputs { if input == nil { - return nil, nil, fmt.Errorf("transaction input [%d] is nil", i) + return nil, fmt.Errorf("transaction input [%d] is nil", i) } if input.Outpoint == nil { - return nil, nil, fmt.Errorf("transaction input [%d] outpoint is nil", i) + return nil, fmt.Errorf("transaction input [%d] outpoint is nil", i) } inputReferences = append( @@ -600,16 +622,17 @@ func extractUnsignedTransactionIOFromTransaction( Vout: input.Outpoint.OutputIndex, }, ) + inputSequences = append(inputSequences, input.Sequence) } outputs := make([]bitcoin.UnsignedTransactionOutput, 0, len(transaction.Outputs)) for i, output := range transaction.Outputs { if output == nil { - return nil, nil, fmt.Errorf("transaction output [%d] is nil", i) + return nil, fmt.Errorf("transaction output [%d] is nil", i) } if output.Value < 0 { - return nil, nil, fmt.Errorf("transaction output [%d] value is negative", i) + return nil, fmt.Errorf("transaction output [%d] value is negative", i) } outputs = append( @@ -621,7 +644,28 @@ func extractUnsignedTransactionIOFromTransaction( ) } - return inputReferences, outputs, nil + return &unsignedTransactionShape{ + Version: transaction.Version, + Locktime: transaction.Locktime, + InputReferences: inputReferences, + InputSequences: inputSequences, + Outputs: outputs, + }, nil +} + +func extractUnsignedTransactionIOFromTransaction( + transaction *bitcoin.Transaction, +) ( + []unsignedTransactionInputReference, + []bitcoin.UnsignedTransactionOutput, + error, +) { + shape, err := extractUnsignedTransactionShapeFromTransaction(transaction) + if err != nil { + return nil, nil, err + } + + return shape.InputReferences, shape.Outputs, nil } func unsignedTransactionInputReferences( @@ -675,6 +719,23 @@ func unsignedTransactionOutputsEqual( return true } +func unsignedTransactionInputSequencesEqual( + first []uint32, + second []uint32, +) bool { + if len(first) != len(second) { + return false + } + + for i := range first { + if first[i] != second[i] { + return false + } + } + + return true +} + // broadcastTransaction broadcasts a signed Bitcoin transaction until // the transaction lands in the Bitcoin mempool or the provided timeout // is hit, whichever comes first. diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index 880ffaf429..27351866c4 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -208,18 +208,24 @@ func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarnin nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( logger, nativeTxHex, - []bitcoin.UnsignedTransactionInput{ - { - TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), - Vout: 7, - ValueSats: 1234, + &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, }, - }, - []bitcoin.UnsignedTransactionOutput{ - { - ScriptPubKeyHex: "0014deadbeef", - ValueSats: 999, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 999, + PublicKeyScript: scriptPubKey, + }, }, + Locktime: 0, }, false, ) @@ -285,18 +291,24 @@ func TestEvaluateNativeUnsignedTransactionForSigning_SubstitutionModeRejectsDive nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( logger, nativeTxHex, - []bitcoin.UnsignedTransactionInput{ - { - TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), - Vout: 7, - ValueSats: 1234, + &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, }, - }, - []bitcoin.UnsignedTransactionOutput{ - { - ScriptPubKeyHex: "0014deadbeef", - ValueSats: 999, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 999, + PublicKeyScript: scriptPubKey, + }, }, + Locktime: 0, }, true, ) @@ -358,27 +370,96 @@ func TestEvaluateNativeUnsignedTransactionForSigning_SubstitutionModeAcceptsMatc nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( logger, nativeTxHex, - []bitcoin.UnsignedTransactionInput{ + nativeTransaction, + true, + ) + if err != nil { + t.Fatalf("unexpected substitution-mode evaluation error: [%v]", err) + } + + if nativeUnsignedTx == nil { + t.Fatal("expected native transaction substitution candidate") + } + + if len(logger.warningMessages) != 0 { + t.Fatalf("unexpected warnings in substitution mode: [%v]", logger.warningMessages) + } +} + +func TestEvaluateNativeUnsignedTransactionForSigning_SubstitutionModeRejectsStructuralDivergence( + t *testing.T, +) { + logger := &warningCaptureLogger{} + + txHashBytes := make([]byte, bitcoin.HashByteLength) + for i := range txHashBytes { + txHashBytes[i] = byte(i + 1) + } + + txHash, err := bitcoin.NewHash(txHashBytes, bitcoin.InternalByteOrder) + if err != nil { + t.Fatalf("cannot build tx hash: [%v]", err) + } + + scriptPubKey := mustDecodeHex(t, "0014deadbeef") + nativeTransaction := &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ { - TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), - Vout: 7, - ValueSats: 1234, + Value: 1000, + PublicKeyScript: scriptPubKey, }, }, - []bitcoin.UnsignedTransactionOutput{ + Locktime: 0, + } + + nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) + + expectedTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ { - ScriptPubKeyHex: "0014deadbeef", - ValueSats: 1000, + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, }, }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + } + + nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( + logger, + nativeTxHex, + expectedTransaction, true, ) - if err != nil { - t.Fatalf("unexpected substitution-mode evaluation error: [%v]", err) + if err == nil { + t.Fatal("expected substitution-mode structural divergence error") } - if nativeUnsignedTx == nil { - t.Fatal("expected native transaction substitution candidate") + if !strings.Contains(err.Error(), "diverges") { + t.Fatalf("unexpected substitution-mode error: [%v]", err) + } + + if nativeUnsignedTx != nil { + t.Fatal("did not expect native transaction on divergence") } if len(logger.warningMessages) != 0 { @@ -540,12 +621,19 @@ func TestWalletTransactionExecutor_SignTransaction_SubstitutesNativeUnsignedTran if len(logger.warningMessages) != 0 { t.Fatalf("unexpected warning logs: [%v]", logger.warningMessages) } + + if !containsLoggedMessage( + logger.infoMessages, + "substituted Go unsigned transaction with native tbtc-signer BuildTaprootTx output", + ) { + t.Fatalf("expected substitution info log, got: [%v]", logger.infoMessages) + } } func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisabled( t *testing.T, ) { - privateKey, unsignedTx, nativeUnsignedTxHex, nativeUnsignedTx := buildTaprootTxSubstitutionFixture(t) + privateKey, unsignedTx, nativeUnsignedTxHex, _ := buildTaprootTxSubstitutionFixture(t) originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn originalSigningSubstitutionEnabledFn := nativeBuildTaprootTxSigningSubstitutionEnabledFn @@ -589,13 +677,6 @@ func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisa ) } - if tx.Version == nativeUnsignedTx.Version { - t.Fatalf( - "did not expect transaction version substitution when gate disabled: [%v]", - tx.Version, - ) - } - if tx.Locktime != 0 { t.Fatalf( "unexpected non-substituted transaction locktime\nexpected: [0]\nactual: [%v]", @@ -603,13 +684,6 @@ func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisa ) } - if tx.Locktime == nativeUnsignedTx.Locktime { - t.Fatalf( - "did not expect transaction locktime substitution when gate disabled: [%v]", - tx.Locktime, - ) - } - if tx.Inputs[0].Sequence != 0xffffffff { t.Fatalf( "unexpected non-substituted input sequence\nexpected: [4294967295]\nactual: [%v]", @@ -617,16 +691,16 @@ func TestWalletTransactionExecutor_SignTransaction_DoesNotSubstituteWhenGateDisa ) } - if tx.Inputs[0].Sequence == nativeUnsignedTx.Inputs[0].Sequence { - t.Fatalf( - "did not expect input sequence substitution when gate disabled: [%v]", - tx.Inputs[0].Sequence, - ) - } - if len(logger.warningMessages) != 0 { t.Fatalf("unexpected warning logs: [%v]", logger.warningMessages) } + + if containsLoggedMessage( + logger.infoMessages, + "substituted Go unsigned transaction with native tbtc-signer BuildTaprootTx output", + ) { + t.Fatalf("did not expect substitution info log when gate disabled: [%v]", logger.infoMessages) + } } func TestWalletTransactionExecutor_SignTransaction_RejectsNativeUnsignedTransactionDivergenceWhenGateEnabled( @@ -701,6 +775,80 @@ func TestWalletTransactionExecutor_SignTransaction_RejectsNativeUnsignedTransact } } +func TestWalletTransactionExecutor_SignTransaction_RejectsNativeUnsignedTransactionStructuralDivergenceWhenGateEnabled( + t *testing.T, +) { + privateKey, unsignedTx, _, nativeUnsignedTx := buildTaprootTxSubstitutionFixture(t) + + divergingNativeUnsignedTx := *nativeUnsignedTx + divergingInputs := make( + []*bitcoin.TransactionInput, + len(nativeUnsignedTx.Inputs), + ) + for i, input := range nativeUnsignedTx.Inputs { + if input == nil { + t.Fatalf("native fixture input [%d] is nil", i) + } + + clonedInput := *input + divergingInputs[i] = &clonedInput + } + divergingNativeUnsignedTx.Inputs = divergingInputs + divergingNativeUnsignedTx.Version = nativeUnsignedTx.Version + 1 + divergingNativeUnsignedTx.Locktime = nativeUnsignedTx.Locktime + 1 + divergingNativeUnsignedTx.Inputs[0].Sequence = nativeUnsignedTx.Inputs[0].Sequence - 1 + divergingNativeUnsignedTxHex := hex.EncodeToString( + divergingNativeUnsignedTx.Serialize(bitcoin.Standard), + ) + + originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn + originalSigningSubstitutionEnabledFn := nativeBuildTaprootTxSigningSubstitutionEnabledFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = originalBuildTaprootTxViaNativeSignerFn + nativeBuildTaprootTxSigningSubstitutionEnabledFn = originalSigningSubstitutionEnabledFn + }) + + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return divergingNativeUnsignedTxHex, nil + } + nativeBuildTaprootTxSigningSubstitutionEnabledFn = func() bool { + return true + } + + wte := &walletTransactionExecutor{ + executingWallet: wallet{ + publicKey: &privateKey.PublicKey, + }, + signingExecutor: &deterministicECDSASigningExecutorForBuildTaprootTxSubstitution{ + privateKey: privateKey, + }, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + + logger := &warningCaptureLogger{} + + tx, err := wte.signTransaction(logger, unsignedTx, 0, 0) + if err == nil { + t.Fatal("expected signTransaction structural divergence error") + } + + if tx != nil { + t.Fatal("expected no signed transaction on substitution structural divergence") + } + + if !strings.Contains(err.Error(), "diverges") { + t.Fatalf("unexpected signTransaction divergence error: [%v]", err) + } + + if len(logger.warningMessages) != 0 { + t.Fatalf("unexpected warning logs in substitution mode: [%v]", logger.warningMessages) + } +} + func buildTaprootTxSubstitutionFixture( t *testing.T, ) ( @@ -769,15 +917,15 @@ func buildTaprootTxSubstitutionFixture( ) nativeUnsignedTx := &bitcoin.Transaction{ - Version: 3, - Locktime: 123, + Version: 1, + Locktime: 0, Inputs: []*bitcoin.TransactionInput{ { Outpoint: &bitcoin.TransactionOutpoint{ TransactionHash: fundingTransaction.Hash(), OutputIndex: 0, }, - Sequence: 0xfffffffd, + Sequence: 0xffffffff, }, }, Outputs: []*bitcoin.TransactionOutput{ @@ -805,6 +953,7 @@ func mustDecodeHex(t *testing.T, value string) []byte { type warningCaptureLogger struct { warningMessages []string + infoMessages []string } func (wcl *warningCaptureLogger) Debug(args ...interface{}) {} @@ -819,9 +968,13 @@ func (wcl *warningCaptureLogger) Fatal(args ...interface{}) {} func (wcl *warningCaptureLogger) Fatalf(format string, args ...interface{}) {} -func (wcl *warningCaptureLogger) Info(args ...interface{}) {} +func (wcl *warningCaptureLogger) Info(args ...interface{}) { + wcl.infoMessages = append(wcl.infoMessages, fmt.Sprint(args...)) +} -func (wcl *warningCaptureLogger) Infof(format string, args ...interface{}) {} +func (wcl *warningCaptureLogger) Infof(format string, args ...interface{}) { + wcl.infoMessages = append(wcl.infoMessages, fmt.Sprintf(format, args...)) +} func (wcl *warningCaptureLogger) Panic(args ...interface{}) {} @@ -836,6 +989,16 @@ func (wcl *warningCaptureLogger) Warnf(format string, args ...interface{}) { ) } +func containsLoggedMessage(messages []string, substring string) bool { + for _, message := range messages { + if strings.Contains(message, substring) { + return true + } + } + + return false +} + type deterministicECDSASigningExecutorForBuildTaprootTxSubstitution struct { privateKey *ecdsa.PrivateKey } From 016f2f7a67701310448f9e2ff9f503569506c5c2 Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 25 Feb 2026 16:11:56 -0600 Subject: [PATCH 81/96] Clean up legacy BuildTaprootTx IO divergence path --- pkg/tbtc/wallet.go | 57 --------- ..._sign_transaction_build_taproot_tx_test.go | 119 ++++++------------ 2 files changed, 39 insertions(+), 137 deletions(-) diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 15e53b1057..285d57604c 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -512,23 +512,6 @@ func decodeNativeUnsignedTransactionHex( return nativeUnsignedTx, nil } -func nativeUnsignedTransactionIODiverges( - nativeUnsignedTxHex string, - expectedInputs []bitcoin.UnsignedTransactionInput, - expectedOutputs []bitcoin.UnsignedTransactionOutput, -) (bool, error) { - nativeUnsignedTx, err := decodeNativeUnsignedTransactionHex(nativeUnsignedTxHex) - if err != nil { - return false, err - } - - return nativeUnsignedTransactionIODivergesFromTransaction( - nativeUnsignedTx, - expectedInputs, - expectedOutputs, - ) -} - func nativeUnsignedTransactionDivergesFromTransaction( nativeUnsignedTx *bitcoin.Transaction, expectedTransaction *bitcoin.Transaction, @@ -560,31 +543,6 @@ func nativeUnsignedTransactionDivergesFromTransaction( nil } -func nativeUnsignedTransactionIODivergesFromTransaction( - nativeUnsignedTx *bitcoin.Transaction, - expectedInputs []bitcoin.UnsignedTransactionInput, - expectedOutputs []bitcoin.UnsignedTransactionOutput, -) (bool, error) { - actualInputReferences, actualOutputs, err := extractUnsignedTransactionIOFromTransaction( - nativeUnsignedTx, - ) - if err != nil { - return false, err - } - - expectedInputReferences := unsignedTransactionInputReferences(expectedInputs) - - return !unsignedTransactionInputReferencesEqual( - expectedInputReferences, - actualInputReferences, - ) || - !unsignedTransactionOutputsEqual( - expectedOutputs, - actualOutputs, - ), - nil -} - type unsignedTransactionShape struct { Version int32 Locktime uint32 @@ -653,21 +611,6 @@ func extractUnsignedTransactionShapeFromTransaction( }, nil } -func extractUnsignedTransactionIOFromTransaction( - transaction *bitcoin.Transaction, -) ( - []unsignedTransactionInputReference, - []bitcoin.UnsignedTransactionOutput, - error, -) { - shape, err := extractUnsignedTransactionShapeFromTransaction(transaction) - if err != nil { - return nil, nil, err - } - - return shape.InputReferences, shape.Outputs, nil -} - func unsignedTransactionInputReferences( inputs []bitcoin.UnsignedTransactionInput, ) []unsignedTransactionInputReference { diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index 27351866c4..21d83b7d70 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -43,7 +43,11 @@ func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( } } -func TestNativeUnsignedTransactionIODiverges_MatchingIO(t *testing.T) { +func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarning( + t *testing.T, +) { + logger := &warningCaptureLogger{} + txHashBytes := make([]byte, bitcoin.HashByteLength) for i := range txHashBytes { txHashBytes[i] = byte(i + 1) @@ -77,97 +81,52 @@ func TestNativeUnsignedTransactionIODiverges_MatchingIO(t *testing.T) { nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) - expectedInputs := []bitcoin.UnsignedTransactionInput{ - { - TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), - Vout: 7, - ValueSats: 1234, - }, - } - expectedOutputs := []bitcoin.UnsignedTransactionOutput{ - { - ScriptPubKeyHex: "0014deadbeef", - ValueSats: 1000, - }, - } - - diverges, err := nativeUnsignedTransactionIODiverges( + nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( + logger, nativeTxHex, - expectedInputs, - expectedOutputs, - ) - if err != nil { - t.Fatalf("unexpected comparison error: [%v]", err) - } - - if diverges { - t.Fatal("expected matching unsigned transaction I/O") - } -} - -func TestNativeUnsignedTransactionIODiverges_MismatchedIO(t *testing.T) { - txHashBytes := make([]byte, bitcoin.HashByteLength) - for i := range txHashBytes { - txHashBytes[i] = byte(i + 1) - } - - txHash, err := bitcoin.NewHash(txHashBytes, bitcoin.InternalByteOrder) - if err != nil { - t.Fatalf("cannot build tx hash: [%v]", err) - } - - scriptPubKey := mustDecodeHex(t, "0014deadbeef") - nativeTransaction := &bitcoin.Transaction{ - Version: 2, - Inputs: []*bitcoin.TransactionInput{ - { - Outpoint: &bitcoin.TransactionOutpoint{ - TransactionHash: txHash, - OutputIndex: 7, + &bitcoin.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, }, - Sequence: 0xffffffff, }, - }, - Outputs: []*bitcoin.TransactionOutput{ - { - Value: 1000, - PublicKeyScript: scriptPubKey, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 999, + PublicKeyScript: scriptPubKey, + }, }, + Locktime: 0, }, - Locktime: 0, + false, + ) + if err != nil { + t.Fatalf("unexpected evaluation error: [%v]", err) } - nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) - - expectedInputs := []bitcoin.UnsignedTransactionInput{ - { - TxIDHex: txHash.Hex(bitcoin.ReversedByteOrder), - Vout: 7, - ValueSats: 1234, - }, - } - expectedOutputs := []bitcoin.UnsignedTransactionOutput{ - { - ScriptPubKeyHex: "0014deadbeef", - ValueSats: 999, - }, + if nativeUnsignedTx != nil { + t.Fatal("did not expect native transaction substitution in observational mode") } - diverges, err := nativeUnsignedTransactionIODiverges( - nativeTxHex, - expectedInputs, - expectedOutputs, - ) - if err != nil { - t.Fatalf("unexpected comparison error: [%v]", err) + if len(logger.warningMessages) != 1 { + t.Fatalf( + "unexpected warning message count\nexpected: [%v]\nactual: [%v]", + 1, + len(logger.warningMessages), + ) } - if !diverges { - t.Fatal("expected unsigned transaction I/O divergence") + if !strings.Contains(logger.warningMessages[0], "diverges") { + t.Fatalf("unexpected warning message: [%v]", logger.warningMessages[0]) } } -func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarning( +func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarningOnStructuralDivergence( t *testing.T, ) { logger := &warningCaptureLogger{} @@ -209,7 +168,7 @@ func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarnin logger, nativeTxHex, &bitcoin.Transaction{ - Version: 2, + Version: 1, Inputs: []*bitcoin.TransactionInput{ { Outpoint: &bitcoin.TransactionOutpoint{ @@ -221,7 +180,7 @@ func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarnin }, Outputs: []*bitcoin.TransactionOutput{ { - Value: 999, + Value: 1000, PublicKeyScript: scriptPubKey, }, }, From 2454c3ecd51d8d17d73f3cddd86c9fa99895a99d Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 25 Feb 2026 16:33:05 -0600 Subject: [PATCH 82/96] Remove dead unsigned input reference converter --- pkg/tbtc/wallet.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 285d57604c..af27357c1e 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -611,23 +611,6 @@ func extractUnsignedTransactionShapeFromTransaction( }, nil } -func unsignedTransactionInputReferences( - inputs []bitcoin.UnsignedTransactionInput, -) []unsignedTransactionInputReference { - result := make([]unsignedTransactionInputReference, 0, len(inputs)) - for _, input := range inputs { - result = append( - result, - unsignedTransactionInputReference{ - TxIDHex: input.TxIDHex, - Vout: input.Vout, - }, - ) - } - - return result -} - func unsignedTransactionInputReferencesEqual( first []unsignedTransactionInputReference, second []unsignedTransactionInputReference, From 7dff0d206b73232b5fb06b30067fb05ae0d547a0 Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 25 Feb 2026 17:41:23 -0600 Subject: [PATCH 83/96] Reject signed data in unsigned transaction replacement --- pkg/bitcoin/transaction_builder.go | 18 ++++-- pkg/bitcoin/transaction_builder_test.go | 77 +++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/pkg/bitcoin/transaction_builder.go b/pkg/bitcoin/transaction_builder.go index 95e5256ec5..7b40758cce 100644 --- a/pkg/bitcoin/transaction_builder.go +++ b/pkg/bitcoin/transaction_builder.go @@ -333,10 +333,6 @@ func (tb *TransactionBuilder) ReplaceUnsignedTransaction( replacedInternal.fromTransaction(transaction) for i := range replacedInternal.TxIn { - if i >= len(previousInputs) { - break - } - previousInput := previousInputs[i] replacedInput := replacedInternal.TxIn[i] @@ -344,6 +340,20 @@ func (tb *TransactionBuilder) ReplaceUnsignedTransaction( continue } + if len(replacedInput.SignatureScript) > 0 { + return fmt.Errorf( + "replacement transaction input [%d] has unexpected non-empty signature script", + i, + ) + } + + if len(replacedInput.Witness) > 0 { + return fmt.Errorf( + "replacement transaction input [%d] has unexpected non-empty witness", + i, + ) + } + if tb.sigHashArgs[i].witness { if len(replacedInput.Witness) == 0 && len(previousInput.Witness) == 1 { redeemScript := append([]byte{}, previousInput.Witness[0]...) diff --git a/pkg/bitcoin/transaction_builder_test.go b/pkg/bitcoin/transaction_builder_test.go index 8fcb810df1..2720febb7d 100644 --- a/pkg/bitcoin/transaction_builder_test.go +++ b/pkg/bitcoin/transaction_builder_test.go @@ -4,6 +4,7 @@ import ( "fmt" "math/big" "reflect" + "strings" "testing" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -374,6 +375,82 @@ func TestTransactionBuilder_ReplaceUnsignedTransaction_RejectsInputMetadataMisma } } +func TestTransactionBuilder_ReplaceUnsignedTransaction_RejectsNonEmptyReplacementSignatureScript( + t *testing.T, +) { + builder := NewTransactionBuilder(nil) + + var txHash chainhash.Hash + builder.internal.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, 0), nil, nil)) + builder.sigHashArgs = append( + builder.sigHashArgs, + &inputSigHashArgs{value: 1, scriptCode: []byte{0x51}, witness: false}, + ) + + err := builder.ReplaceUnsignedTransaction( + &Transaction{ + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash(txHash), + OutputIndex: 0, + }, + SignatureScript: []byte{0xaa}, + Sequence: 0xffffffff, + }, + }, + }, + ) + if err == nil { + t.Fatal("expected replacement signature script error") + } + + if !strings.Contains( + err.Error(), + "replacement transaction input [0] has unexpected non-empty signature script", + ) { + t.Fatalf("unexpected error: [%v]", err) + } +} + +func TestTransactionBuilder_ReplaceUnsignedTransaction_RejectsNonEmptyReplacementWitness( + t *testing.T, +) { + builder := NewTransactionBuilder(nil) + + var txHash chainhash.Hash + builder.internal.AddTxIn(wire.NewTxIn(wire.NewOutPoint(&txHash, 0), nil, nil)) + builder.sigHashArgs = append( + builder.sigHashArgs, + &inputSigHashArgs{value: 1, scriptCode: []byte{0x51}, witness: true}, + ) + + err := builder.ReplaceUnsignedTransaction( + &Transaction{ + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash(txHash), + OutputIndex: 0, + }, + Witness: wire.TxWitness{[]byte{0xbb}}, + Sequence: 0xffffffff, + }, + }, + }, + ) + if err == nil { + t.Fatal("expected replacement witness error") + } + + if !strings.Contains( + err.Error(), + "replacement transaction input [0] has unexpected non-empty witness", + ) { + t.Fatalf("unexpected error: [%v]", err) + } +} + func TestTransactionBuilder_UnsignedTransactionIO(t *testing.T) { builder := NewTransactionBuilder(nil) From 4490ac483f8353ab2a6c3beda319c0525d029795 Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 26 Feb 2026 11:27:41 -0600 Subject: [PATCH 84/96] Add detailed BuildTaprootTx divergence diagnostics --- ...ild_taproot_tx_frost_native_tbtc_signer.go | 2 + pkg/tbtc/wallet.go | 186 ++++++++++++------ ..._sign_transaction_build_taproot_tx_test.go | 24 +++ 3 files changed, 152 insertions(+), 60 deletions(-) diff --git a/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go index a615346856..ab73530ff5 100644 --- a/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go +++ b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go @@ -92,6 +92,8 @@ func buildTaprootTxSessionID( ) string { // Session ID is deterministically derived from Go-side transaction I/O using // encoding/json. Rust currently treats this session_id as opaque. + // If input/output schema changes in a future migration phase, update this + // derivation intentionally to avoid silent cross-version session ID drift. sessionPayload, err := json.Marshal(struct { Inputs []bitcoin.UnsignedTransactionInput `json:"inputs"` Outputs []bitcoin.UnsignedTransactionOutput `json:"outputs"` diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index af27357c1e..194b44149f 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -461,7 +461,7 @@ func evaluateNativeUnsignedTransactionForSigning( return nil, nil } - diverges, err := nativeUnsignedTransactionDivergesFromTransaction( + diverges, divergenceReason, err := nativeUnsignedTransactionDivergesFromTransaction( nativeUnsignedTx, expectedTransaction, ) @@ -478,15 +478,20 @@ func evaluateNativeUnsignedTransactionForSigning( } if diverges { - if substitutionEnabled { - return nil, fmt.Errorf( - "native BuildTaprootTx unsigned transaction diverges from Go builder state", + divergenceMessage := "native BuildTaprootTx unsigned transaction diverges from Go builder state" + if divergenceReason != "" { + divergenceMessage = fmt.Sprintf( + "%s: %s", + divergenceMessage, + divergenceReason, ) } - signTxLogger.Warnf( - "native BuildTaprootTx unsigned transaction diverges from Go builder state", - ) + if substitutionEnabled { + return nil, fmt.Errorf("%s", divergenceMessage) + } + + signTxLogger.Warnf(divergenceMessage) } if substitutionEnabled { @@ -515,32 +520,83 @@ func decodeNativeUnsignedTransactionHex( func nativeUnsignedTransactionDivergesFromTransaction( nativeUnsignedTx *bitcoin.Transaction, expectedTransaction *bitcoin.Transaction, -) (bool, error) { +) (bool, string, error) { actualShape, err := extractUnsignedTransactionShapeFromTransaction(nativeUnsignedTx) if err != nil { - return false, err + return false, "", err } expectedShape, err := extractUnsignedTransactionShapeFromTransaction(expectedTransaction) if err != nil { - return false, err + return false, "", err + } + + if actualShape.Version != expectedShape.Version { + return true, fmt.Sprintf( + "version mismatch: expected [%d], got [%d]", + expectedShape.Version, + actualShape.Version, + ), nil + } + + if actualShape.Locktime != expectedShape.Locktime { + return true, fmt.Sprintf( + "locktime mismatch: expected [%d], got [%d]", + expectedShape.Locktime, + actualShape.Locktime, + ), nil + } + + if reason, diverges := unsignedTransactionInputReferencesDivergenceReason( + actualShape.InputReferences, + expectedShape.InputReferences, + ); diverges { + return true, reason, nil + } + + if reason, diverges := unsignedTransactionInputSequencesDivergenceReason( + actualShape.InputSequences, + expectedShape.InputSequences, + ); diverges { + return true, reason, nil + } + + if reason, diverges := unsignedTransactionOutputsDivergenceReason( + actualShape.Outputs, + expectedShape.Outputs, + ); diverges { + return true, reason, nil + } + + return false, "", nil +} + +func unsignedTransactionInputReferencesDivergenceReason( + actual []unsignedTransactionInputReference, + expected []unsignedTransactionInputReference, +) (string, bool) { + if len(actual) != len(expected) { + return fmt.Sprintf( + "input reference count mismatch: expected [%d], got [%d]", + len(expected), + len(actual), + ), true + } + + for i := range actual { + if actual[i] != expected[i] { + return fmt.Sprintf( + "input reference mismatch at index [%d]: expected [%s:%d], got [%s:%d]", + i, + expected[i].TxIDHex, + expected[i].Vout, + actual[i].TxIDHex, + actual[i].Vout, + ), true + } } - return actualShape.Version != expectedShape.Version || - actualShape.Locktime != expectedShape.Locktime || - !unsignedTransactionInputReferencesEqual( - actualShape.InputReferences, - expectedShape.InputReferences, - ) || - !unsignedTransactionInputSequencesEqual( - actualShape.InputSequences, - expectedShape.InputSequences, - ) || - !unsignedTransactionOutputsEqual( - actualShape.Outputs, - expectedShape.Outputs, - ), - nil + return "", false } type unsignedTransactionShape struct { @@ -611,55 +667,65 @@ func extractUnsignedTransactionShapeFromTransaction( }, nil } -func unsignedTransactionInputReferencesEqual( - first []unsignedTransactionInputReference, - second []unsignedTransactionInputReference, -) bool { - if len(first) != len(second) { - return false +func unsignedTransactionOutputsDivergenceReason( + actual []bitcoin.UnsignedTransactionOutput, + expected []bitcoin.UnsignedTransactionOutput, +) (string, bool) { + if len(actual) != len(expected) { + return fmt.Sprintf( + "output count mismatch: expected [%d], got [%d]", + len(expected), + len(actual), + ), true } - for i := range first { - if first[i] != second[i] { - return false + for i := range actual { + if actual[i].ValueSats != expected[i].ValueSats { + return fmt.Sprintf( + "output value mismatch at index [%d]: expected [%d], got [%d]", + i, + expected[i].ValueSats, + actual[i].ValueSats, + ), true } - } - - return true -} -func unsignedTransactionOutputsEqual( - first []bitcoin.UnsignedTransactionOutput, - second []bitcoin.UnsignedTransactionOutput, -) bool { - if len(first) != len(second) { - return false - } - - for i := range first { - if first[i] != second[i] { - return false + if actual[i].ScriptPubKeyHex != expected[i].ScriptPubKeyHex { + return fmt.Sprintf( + "output script mismatch at index [%d]: expected [%s], got [%s]", + i, + expected[i].ScriptPubKeyHex, + actual[i].ScriptPubKeyHex, + ), true } } - return true + return "", false } -func unsignedTransactionInputSequencesEqual( - first []uint32, - second []uint32, -) bool { - if len(first) != len(second) { - return false +func unsignedTransactionInputSequencesDivergenceReason( + actual []uint32, + expected []uint32, +) (string, bool) { + if len(actual) != len(expected) { + return fmt.Sprintf( + "input sequence count mismatch: expected [%d], got [%d]", + len(expected), + len(actual), + ), true } - for i := range first { - if first[i] != second[i] { - return false + for i := range actual { + if actual[i] != expected[i] { + return fmt.Sprintf( + "input sequence mismatch at index [%d]: expected [%d], got [%d]", + i, + expected[i], + actual[i], + ), true } } - return true + return "", false } // broadcastTransaction broadcasts a signed Bitcoin transaction until diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index 21d83b7d70..fd03674d15 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -124,6 +124,10 @@ func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarnin if !strings.Contains(logger.warningMessages[0], "diverges") { t.Fatalf("unexpected warning message: [%v]", logger.warningMessages[0]) } + + if !strings.Contains(logger.warningMessages[0], "output value mismatch") { + t.Fatalf("missing divergence detail in warning: [%v]", logger.warningMessages[0]) + } } func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarningOnStructuralDivergence( @@ -207,6 +211,10 @@ func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarnin if !strings.Contains(logger.warningMessages[0], "diverges") { t.Fatalf("unexpected warning message: [%v]", logger.warningMessages[0]) } + + if !strings.Contains(logger.warningMessages[0], "version mismatch") { + t.Fatalf("missing divergence detail in warning: [%v]", logger.warningMessages[0]) + } } func TestEvaluateNativeUnsignedTransactionForSigning_SubstitutionModeRejectsDivergence( @@ -279,6 +287,10 @@ func TestEvaluateNativeUnsignedTransactionForSigning_SubstitutionModeRejectsDive t.Fatalf("unexpected substitution-mode error: [%v]", err) } + if !strings.Contains(err.Error(), "output value mismatch") { + t.Fatalf("missing divergence detail in substitution error: [%v]", err) + } + if nativeUnsignedTx != nil { t.Fatal("did not expect native transaction on divergence") } @@ -417,6 +429,10 @@ func TestEvaluateNativeUnsignedTransactionForSigning_SubstitutionModeRejectsStru t.Fatalf("unexpected substitution-mode error: [%v]", err) } + if !strings.Contains(err.Error(), "version mismatch") { + t.Fatalf("missing divergence detail in substitution error: [%v]", err) + } + if nativeUnsignedTx != nil { t.Fatal("did not expect native transaction on divergence") } @@ -729,6 +745,10 @@ func TestWalletTransactionExecutor_SignTransaction_RejectsNativeUnsignedTransact t.Fatalf("unexpected signTransaction divergence error: [%v]", err) } + if !strings.Contains(err.Error(), "output value mismatch") { + t.Fatalf("missing divergence detail in signTransaction error: [%v]", err) + } + if len(logger.warningMessages) != 0 { t.Fatalf("unexpected warning logs in substitution mode: [%v]", logger.warningMessages) } @@ -803,6 +823,10 @@ func TestWalletTransactionExecutor_SignTransaction_RejectsNativeUnsignedTransact t.Fatalf("unexpected signTransaction divergence error: [%v]", err) } + if !strings.Contains(err.Error(), "version mismatch") { + t.Fatalf("missing divergence detail in signTransaction error: [%v]", err) + } + if len(logger.warningMessages) != 0 { t.Fatalf("unexpected warning logs in substitution mode: [%v]", logger.warningMessages) } From 69b5ffa059e61821c00396bf3f241b52dad9f674 Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 26 Feb 2026 11:52:28 -0600 Subject: [PATCH 85/96] Classify tbtc-signer operation errors as bridge failures --- ...e_tbtc_signer_registration_frost_native.go | 2 +- ...c_signer_registration_frost_native_test.go | 85 +++++++++++++++---- ...ild_taproot_tx_frost_native_tbtc_signer.go | 2 - 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index 05237ca3bc..c4fddad53c 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -337,7 +337,7 @@ func buildTaggedTBTCSignerOperationError( ) error { return fmt.Errorf( "%w: tbtc-signer bridge operation [%v] failed: [%s]", - ErrNativeCryptographyUnavailable, + ErrNativeBridgeOperationFailed, operation, message, ) diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 39f2b0e224..4d59d5ad0e 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -268,10 +268,17 @@ func TestBuildTaggedTBTCSignerRunDKGRequestPayload_RejectsInvalidInput(t *testin t.Fatal("expected payload build error") } - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if !errors.Is(err, ErrNativeBridgeOperationFailed) { t.Fatalf( - "expected native cryptography unavailable error: [%v], got [%v]", - ErrNativeCryptographyUnavailable, + "expected native bridge operation failed error: [%v], got [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", err, ) } @@ -408,10 +415,17 @@ func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_EmptySessionID(t *tes "key-group-1", nil, ) - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if !errors.Is(err, ErrNativeBridgeOperationFailed) { t.Fatalf( - "expected native cryptography unavailable error: [%v], got [%v]", - ErrNativeCryptographyUnavailable, + "expected native bridge operation failed error: [%v], got [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", err, ) } @@ -425,10 +439,17 @@ func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_ZeroMemberID(t *testi "key-group-1", nil, ) - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if !errors.Is(err, ErrNativeBridgeOperationFailed) { t.Fatalf( - "expected native cryptography unavailable error: [%v], got [%v]", - ErrNativeCryptographyUnavailable, + "expected native bridge operation failed error: [%v], got [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", err, ) } @@ -581,10 +602,17 @@ func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse_RejectsZeroSigningPar t.Fatal("expected error") } - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if !errors.Is(err, ErrNativeBridgeOperationFailed) { t.Fatalf( "unexpected error\nexpected: [%v]\nactual: [%v]", - ErrNativeCryptographyUnavailable, + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", err, ) } @@ -602,10 +630,17 @@ func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse_RejectsDuplicateSigni t.Fatal("expected error") } - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if !errors.Is(err, ErrNativeBridgeOperationFailed) { t.Fatalf( "unexpected error\nexpected: [%v]\nactual: [%v]", - ErrNativeCryptographyUnavailable, + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", err, ) } @@ -623,10 +658,17 @@ func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse_RejectsZeroOwnContrib t.Fatal("expected error") } - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if !errors.Is(err, ErrNativeBridgeOperationFailed) { t.Fatalf( "unexpected error\nexpected: [%v]\nactual: [%v]", - ErrNativeCryptographyUnavailable, + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", err, ) } @@ -818,10 +860,17 @@ func TestBuildTaggedTBTCSignerBuildTaprootTxRequestPayload_RejectsInvalidInput( t.Fatal("expected payload build error") } - if !errors.Is(err, ErrNativeCryptographyUnavailable) { + if !errors.Is(err, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "expected native bridge operation failed error: [%v], got [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { t.Fatalf( - "expected native cryptography unavailable error: [%v], got [%v]", - ErrNativeCryptographyUnavailable, + "did not expect native cryptography unavailable error: [%v]", err, ) } diff --git a/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go index ab73530ff5..30658c0715 100644 --- a/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go +++ b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go @@ -58,8 +58,6 @@ func buildTaprootTxViaNativeSigner( if err != nil { // Keep legacy fallback behavior for the observational BuildTaprootTx // phase when native bridge support is unavailable. - // Note that current bridge error mapping can also classify operational - // failures as unavailable; tighten this split before signing-substitution. if errors.Is(err, frostsigning.ErrNativeCryptographyUnavailable) { return "", nil } From 1c2ea9f4018c9e9611d1bb9af974850f7e8eaa0b Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 26 Feb 2026 12:01:48 -0600 Subject: [PATCH 86/96] Apply review follow-ups for bridge error taxonomy --- ...e_tbtc_signer_registration_frost_native.go | 14 +------ pkg/tbtc/wallet.go | 2 +- ..._sign_transaction_build_taproot_tx_test.go | 38 +++++++++++++++++++ 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index c4fddad53c..9ba7836fde 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -343,18 +343,6 @@ func buildTaggedTBTCSignerOperationError( ) } -func buildTaggedTBTCSignerBridgeOperationError( - operation string, - message string, -) error { - return fmt.Errorf( - "%w: tbtc-signer bridge operation [%v] failed: [%s]", - ErrNativeBridgeOperationFailed, - operation, - message, - ) -} - func buildTaggedTBTCSignerRunDKGRequestPayload( sessionID string, participants []NativeTBTCSignerDKGParticipant, @@ -973,7 +961,7 @@ func buildTaggedTBTCSignerResultStatusError( } if statusCode != 0 { - return buildTaggedTBTCSignerBridgeOperationError( + return buildTaggedTBTCSignerOperationError( operation, buildTaggedTBTCSignerErrorMessage(payload), ) diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 194b44149f..561f8dab9c 100644 --- a/pkg/tbtc/wallet.go +++ b/pkg/tbtc/wallet.go @@ -336,7 +336,7 @@ func (wte *walletTransactionExecutor) signTransaction( nativeUnsignedTxHex, err := buildTaprootTxViaNativeSignerFn(unsignedTx) if err != nil { return nil, fmt.Errorf( - "error while building unsigned transaction with native tbtc-signer: [%v]", + "error while building unsigned transaction with native tbtc-signer: [%w]", err, ) } diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index fd03674d15..67adb77253 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -14,6 +14,7 @@ import ( "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/frost" + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" "github.com/keep-network/keep-core/pkg/tecdsa" ) @@ -43,6 +44,43 @@ func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( } } +func TestWalletTransactionExecutor_SignTransaction_PropagatesBuildTaprootTxBridgeOperationError( + t *testing.T, +) { + original := buildTaprootTxViaNativeSignerFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = original + }) + + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return "", fmt.Errorf( + "%w: operation failed", + frostsigning.ErrNativeBridgeOperationFailed, + ) + } + + wte := &walletTransactionExecutor{} + + _, err := wte.signTransaction(nil, nil, 0, 0) + if err == nil { + t.Fatal("expected signTransaction error") + } + + if !errors.Is(err, frostsigning.ErrNativeBridgeOperationFailed) { + t.Fatalf( + "expected bridge operation failure error: [%v], got [%v]", + frostsigning.ErrNativeBridgeOperationFailed, + err, + ) + } + + if !strings.Contains(err.Error(), "native tbtc-signer") { + t.Fatalf("unexpected error: [%v]", err) + } +} + func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarning( t *testing.T, ) { From 8da42532e9fe1e78c46792b5066e8befc024c2cc Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 26 Feb 2026 12:09:35 -0600 Subject: [PATCH 87/96] Harden BuildTaprootTx error-path tests --- ..._sign_transaction_build_taproot_tx_test.go | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go index 67adb77253..c935c9c42b 100644 --- a/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -21,6 +21,8 @@ import ( func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( t *testing.T, ) { + privateKey, unsignedTx, _, _ := buildTaprootTxSubstitutionFixture(t) + original := buildTaprootTxViaNativeSignerFn t.Cleanup(func() { buildTaprootTxViaNativeSignerFn = original @@ -32,9 +34,18 @@ func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( return "", errors.New("build tx failed") } - wte := &walletTransactionExecutor{} + wte := &walletTransactionExecutor{ + executingWallet: wallet{ + publicKey: &privateKey.PublicKey, + }, + signingExecutor: &unexpectedSigningExecutorForBuildTaprootTxError{}, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + logger := &warningCaptureLogger{} - _, err := wte.signTransaction(nil, nil, 0, 0) + _, err := wte.signTransaction(logger, unsignedTx, 0, 0) if err == nil { t.Fatal("expected signTransaction error") } @@ -47,6 +58,8 @@ func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( func TestWalletTransactionExecutor_SignTransaction_PropagatesBuildTaprootTxBridgeOperationError( t *testing.T, ) { + privateKey, unsignedTx, _, _ := buildTaprootTxSubstitutionFixture(t) + original := buildTaprootTxViaNativeSignerFn t.Cleanup(func() { buildTaprootTxViaNativeSignerFn = original @@ -61,9 +74,18 @@ func TestWalletTransactionExecutor_SignTransaction_PropagatesBuildTaprootTxBridg ) } - wte := &walletTransactionExecutor{} + wte := &walletTransactionExecutor{ + executingWallet: wallet{ + publicKey: &privateKey.PublicKey, + }, + signingExecutor: &unexpectedSigningExecutorForBuildTaprootTxError{}, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + logger := &warningCaptureLogger{} - _, err := wte.signTransaction(nil, nil, 0, 0) + _, err := wte.signTransaction(logger, unsignedTx, 0, 0) if err == nil { t.Fatal("expected signTransaction error") } @@ -1052,3 +1074,13 @@ func (desefbts *deterministicECDSASigningExecutorForBuildTaprootTxSubstitution) return signatures, nil } + +type unexpectedSigningExecutorForBuildTaprootTxError struct{} + +func (usefbte *unexpectedSigningExecutorForBuildTaprootTxError) signBatch( + ctx context.Context, + messages []*big.Int, + startBlock uint64, +) ([]*frost.Signature, error) { + return nil, errors.New("unexpected signBatch invocation") +} From d4e95c5f35ab6c2e1edaf6311ba7a0f29117e99c Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 26 Feb 2026 14:06:36 -0600 Subject: [PATCH 88/96] Remove UniFFI SDK dependency path from frost-native build --- go.mod | 3 - go.sum | 2 - ...e_tbtc_signer_registration_frost_native.go | 2 +- ...c_signer_registration_frost_native_test.go | 2 +- ...niffi_registration_frost_native_default.go | 2 +- ...uniffi_registration_frost_native_uniffi.go | 174 +----------------- ...i_registration_frost_native_uniffi_test.go | 113 +----------- pkg/tbtc/signer_material_encoding.go | 4 +- ...ner_material_encoding_frost_native_test.go | 75 ++++---- ...er_material_resolver_build_frost_native.go | 23 ++- ...terial_resolver_build_frost_native_test.go | 75 ++++---- 11 files changed, 97 insertions(+), 378 deletions(-) diff --git a/go.mod b/go.mod index 99232808c6..802a5e4a2e 100644 --- a/go.mod +++ b/go.mod @@ -203,7 +203,6 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect - github.com/zecdev/frost-uniffi-sdk v0.0.0-20260221162625-51e08b3fb886 go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect @@ -226,5 +225,3 @@ require ( lukechampine.com/blake3 v1.3.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) - -replace github.com/zecdev/frost-uniffi-sdk => github.com/tlabs-xyz/frost-uniffi-sdk v0.0.0-20260221162625-51e08b3fb886 diff --git a/go.sum b/go.sum index 652ed1a3d7..57c959803b 100644 --- a/go.sum +++ b/go.sum @@ -767,8 +767,6 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/tlabs-xyz/frost-uniffi-sdk v0.0.0-20260221162625-51e08b3fb886 h1:A4ZWyfNci/u+tnld6gtl419eBGtECIMPwIAKqsc6nQQ= -github.com/tlabs-xyz/frost-uniffi-sdk v0.0.0-20260221162625-51e08b3fb886/go.mod h1:90FbRr9Nyr8Zf3LRwGG8eISJJ1xhq4HXmkTMqAqsEz8= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index 9ba7836fde..b97b693ad1 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -1,4 +1,4 @@ -//go:build frost_native && frost_tbtc_signer && cgo && !frost_uniffi_sdk +//go:build frost_native && frost_tbtc_signer && cgo package signing diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 4d59d5ad0e..3e49e0b529 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -1,4 +1,4 @@ -//go:build frost_native && frost_tbtc_signer && cgo && !frost_uniffi_sdk +//go:build frost_native && frost_tbtc_signer && cgo package signing diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go index 532c86b3fa..fa32548b7b 100644 --- a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go @@ -1,4 +1,4 @@ -//go:build frost_native && !(frost_uniffi_sdk && cgo) && !(frost_tbtc_signer && cgo) +//go:build frost_native && !(frost_tbtc_signer && cgo) && !(frost_uniffi_sdk && cgo && frost_uniffi_legacy) package signing diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go index 6d7aa80051..896fee1a7f 100644 --- a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go @@ -1,177 +1,7 @@ -//go:build frost_native && frost_uniffi_sdk && cgo +//go:build frost_native && frost_uniffi_sdk && cgo && frost_uniffi_legacy package signing -import ( - "fmt" - - frostuniffi "github.com/zecdev/frost-uniffi-sdk/frost_go_ffi" -) - -type buildTaggedUniFFINativeFROSTBridge struct{} - func registerBuildTaggedNativeFROSTSigningEngine() error { - engine, err := newUniFFINativeFROSTSigningEngine( - &buildTaggedUniFFINativeFROSTBridge{}, - ) - if err != nil { - return err - } - - return RegisterNativeFROSTSigningEngine(engine) -} - -func recoverUniFFIPanic(err *error) { - if r := recover(); r != nil { - *err = fmt.Errorf("uniffi panic: [%v]", r) - } -} - -func (btnufb *buildTaggedUniFFINativeFROSTBridge) GenerateNoncesAndCommitments( - keyPackageIdentifier string, - keyPackageData []byte, -) ( - noncesData []byte, - commitmentIdentifier string, - commitmentData []byte, - err error, -) { - defer recoverUniFFIPanic(&err) - - firstRoundCommitment, err := frostuniffi.GenerateNoncesAndCommitments( - frostuniffi.FrostKeyPackage{ - Identifier: frostuniffi.ParticipantIdentifier{ - Data: keyPackageIdentifier, - }, - Data: append([]byte{}, keyPackageData...), - }, - ) - if err != nil { - return nil, "", nil, fmt.Errorf( - "cannot generate nonces and commitments: [%w]", - err, - ) - } - - return append([]byte{}, firstRoundCommitment.Nonces.Data...), - firstRoundCommitment.Commitments.Identifier.Data, - append([]byte{}, firstRoundCommitment.Commitments.Data...), - nil -} - -func (btnufb *buildTaggedUniFFINativeFROSTBridge) NewSigningPackage( - message []byte, - commitments []uniFFINativeFROSTCommitment, -) (signingPackageData []byte, err error) { - defer recoverUniFFIPanic(&err) - - uniffiCommitments := make( - []frostuniffi.FrostSigningCommitments, - 0, - len(commitments), - ) - - for _, commitment := range commitments { - uniffiCommitments = append( - uniffiCommitments, - frostuniffi.FrostSigningCommitments{ - Identifier: frostuniffi.ParticipantIdentifier{ - Data: commitment.Identifier, - }, - Data: append([]byte{}, commitment.Data...), - }, - ) - } - - signingPackage, err := frostuniffi.NewSigningPackage( - frostuniffi.Message{ - Data: append([]byte{}, message...), - }, - uniffiCommitments, - ) - if err != nil { - return nil, fmt.Errorf("cannot build signing package: [%w]", err) - } - - return append([]byte{}, signingPackage.Data...), nil -} - -func (btnufb *buildTaggedUniFFINativeFROSTBridge) Sign( - signingPackageData []byte, - noncesData []byte, - keyPackageIdentifier string, - keyPackageData []byte, -) (signatureShareIdentifier string, signatureShareData []byte, err error) { - defer recoverUniFFIPanic(&err) - - signatureShare, err := frostuniffi.Sign( - frostuniffi.FrostSigningPackage{ - Data: append([]byte{}, signingPackageData...), - }, - frostuniffi.FrostSigningNonces{ - Data: append([]byte{}, noncesData...), - }, - frostuniffi.FrostKeyPackage{ - Identifier: frostuniffi.ParticipantIdentifier{ - Data: keyPackageIdentifier, - }, - Data: append([]byte{}, keyPackageData...), - }, - ) - if err != nil { - return "", nil, fmt.Errorf("cannot produce signature share: [%w]", err) - } - - return signatureShare.Identifier.Data, append([]byte{}, signatureShare.Data...), nil -} - -func (btnufb *buildTaggedUniFFINativeFROSTBridge) Aggregate( - signingPackageData []byte, - signatureShares []uniFFINativeFROSTSignatureShare, - publicKeyPackage *NativeFROSTPublicKeyPackage, -) (signature []byte, err error) { - defer recoverUniFFIPanic(&err) - - uniffiSignatureShares := make( - []frostuniffi.FrostSignatureShare, - 0, - len(signatureShares), - ) - for _, signatureShare := range signatureShares { - uniffiSignatureShares = append( - uniffiSignatureShares, - frostuniffi.FrostSignatureShare{ - Identifier: frostuniffi.ParticipantIdentifier{ - Data: signatureShare.Identifier, - }, - Data: append([]byte{}, signatureShare.Data...), - }, - ) - } - - uniffiVerifyingShares := make( - map[frostuniffi.ParticipantIdentifier]string, - len(publicKeyPackage.VerifyingShares), - ) - for identifier, verifyingShare := range publicKeyPackage.VerifyingShares { - uniffiVerifyingShares[frostuniffi.ParticipantIdentifier{ - Data: identifier, - }] = verifyingShare - } - - resultSignature, err := frostuniffi.Aggregate( - frostuniffi.FrostSigningPackage{ - Data: append([]byte{}, signingPackageData...), - }, - uniffiSignatureShares, - frostuniffi.FrostPublicKeyPackage{ - VerifyingShares: uniffiVerifyingShares, - VerifyingKey: publicKeyPackage.VerifyingKey, - }, - ) - if err != nil { - return nil, fmt.Errorf("cannot aggregate signature shares: [%w]", err) - } - - return append([]byte{}, resultSignature.Data...), nil + return nil } diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go index 0f80fc3168..63b3d2caab 100644 --- a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go @@ -1,113 +1,14 @@ -//go:build frost_native && frost_uniffi_sdk && cgo +//go:build frost_native && frost_uniffi_sdk && cgo && frost_uniffi_legacy package signing -import ( - "testing" +import "testing" - frostuniffi "github.com/zecdev/frost-uniffi-sdk/frost_go_ffi" -) - -func TestBuildTaggedUniFFINativeFROSTBridge_EndToEndSigning(t *testing.T) { - engine, err := newUniFFINativeFROSTSigningEngine( - &buildTaggedUniFFINativeFROSTBridge{}, - ) - if err != nil { - t.Fatalf("unexpected engine constructor error: [%v]", err) - } - - keygen, err := frostuniffi.TrustedDealerKeygenFrom( - frostuniffi.Configuration{ - MinSigners: 2, - MaxSigners: 2, - Secret: []byte{}, - }, - ) - if err != nil { - t.Fatalf("cannot generate trusted dealer key shares: [%v]", err) - } - - keyPackages := make([]*NativeFROSTKeyPackage, 0, len(keygen.SecretShares)) - for _, secretShare := range keygen.SecretShares { - keyPackage, err := frostuniffi.VerifyAndGetKeyPackageFrom(secretShare) - if err != nil { - t.Fatalf("cannot verify key package from secret share: [%v]", err) - } - - keyPackages = append( - keyPackages, - &NativeFROSTKeyPackage{ - Identifier: keyPackage.Identifier.Data, - Data: append([]byte{}, keyPackage.Data...), - }, - ) - } - - if len(keyPackages) != 2 { - t.Fatalf( - "unexpected key package count\nexpected: [%v]\nactual: [%v]", - 2, - len(keyPackages), - ) - } - - nonces := make([]*NativeFROSTNonces, 0, len(keyPackages)) - commitments := make([]*NativeFROSTCommitment, 0, len(keyPackages)) - for _, keyPackage := range keyPackages { - generatedNonces, generatedCommitment, err := engine.GenerateNoncesAndCommitments( - keyPackage, - ) - if err != nil { - t.Fatalf("cannot generate nonces and commitments: [%v]", err) - } - - nonces = append(nonces, generatedNonces) - commitments = append(commitments, generatedCommitment) - } - - message := []byte("keep-core uniffi bridge integration test") - signingPackage, err := engine.NewSigningPackage(message, commitments) - if err != nil { - t.Fatalf("cannot build signing package: [%v]", err) - } - - signatureShares := make([]*NativeFROSTSignatureShare, 0, len(keyPackages)) - for i, keyPackage := range keyPackages { - signatureShare, err := engine.Sign(signingPackage, nonces[i], keyPackage) - if err != nil { - t.Fatalf("cannot produce signature share: [%v]", err) - } - - signatureShares = append(signatureShares, signatureShare) - } - - verifyingShares := make(map[string]string, len(keygen.PublicKeyPackage.VerifyingShares)) - for identifier, verifyingShare := range keygen.PublicKeyPackage.VerifyingShares { - verifyingShares[identifier.Data] = verifyingShare - } - - signatureBytes, err := engine.Aggregate( - signingPackage, - signatureShares, - &NativeFROSTPublicKeyPackage{ - VerifyingShares: verifyingShares, - VerifyingKey: keygen.PublicKeyPackage.VerifyingKey, - }, - ) - if err != nil { - t.Fatalf("cannot aggregate signature shares: [%v]", err) - } - - err = frostuniffi.VerifySignature( - frostuniffi.Message{ - Data: message, - }, - frostuniffi.FrostSignature{ - Data: signatureBytes, - }, - keygen.PublicKeyPackage, - ) +func TestRegisterBuildTaggedNativeFROSTSigningEngine_UniFFILegacyNoop( + t *testing.T, +) { + err := registerBuildTaggedNativeFROSTSigningEngine() if err != nil { - t.Fatalf("cannot verify aggregated signature: [%v]", err) + t.Fatalf("unexpected registration error: [%v]", err) } } diff --git a/pkg/tbtc/signer_material_encoding.go b/pkg/tbtc/signer_material_encoding.go index c4a416abbc..f4275896a7 100644 --- a/pkg/tbtc/signer_material_encoding.go +++ b/pkg/tbtc/signer_material_encoding.go @@ -51,8 +51,8 @@ func marshalSignerMaterialForPersistence( material.Payload, ) case []byte: - // Transitional compatibility: raw bytes are treated as - // frost-uniffi-v1 payloads produced by default resolver paths. + // Transitional compatibility: raw bytes are treated as legacy + // frost-uniffi-v1 payloads from previously persisted signer entries. return encodeNativeSignerMaterialForPersistence( frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, material, diff --git a/pkg/tbtc/signer_material_encoding_frost_native_test.go b/pkg/tbtc/signer_material_encoding_frost_native_test.go index 324e854bcd..e6bcbd8caf 100644 --- a/pkg/tbtc/signer_material_encoding_frost_native_test.go +++ b/pkg/tbtc/signer_material_encoding_frost_native_test.go @@ -52,55 +52,42 @@ func TestUnmarshalSignerMaterialFromPersistence_LegacyEncodingResolvesNativeMate ) } - var actualPayload []byte - switch nativeSignerMaterial.Format { - case frostsigning.NativeSignerMaterialFormatFrostUniFFIV1: - decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} - if err := decodedPrivateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { - t.Fatalf("failed unmarshalling native signer material payload: [%v]", err) - } - - actualPayload, err = decodedPrivateKeyShare.Marshal() - if err != nil { - t.Fatalf("failed marshaling decoded private key share: [%v]", err) - } - - case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: - var payload frostsigning.NativeTBTCSignerMaterialPayload - if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { - t.Fatalf("failed unmarshalling tbtc signer material payload: [%v]", err) - } - - if payload.KeyGroup == "" { - t.Fatal("expected non-empty tbtc-signer key group") - } - - if payload.KeyGroupSource == "" { - t.Fatal("expected non-empty tbtc-signer key group source") - } - - legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) - if err != nil { - t.Fatalf("failed decoding legacy private key share payload: [%v]", err) - } - - decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} - if err := decodedPrivateKeyShare.Unmarshal(legacyPrivateKeySharePayload); err != nil { - t.Fatalf("failed unmarshalling decoded private key share: [%v]", err) - } - - actualPayload, err = decodedPrivateKeyShare.Marshal() - if err != nil { - t.Fatalf("failed marshaling decoded private key share: [%v]", err) - } - - default: + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1 { t.Fatalf( - "unexpected signer material format\nactual: [%v]", + "unexpected signer material format\nexpected: [%v]\nactual: [%v]", + frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, nativeSignerMaterial.Format, ) } + var payload frostsigning.NativeTBTCSignerMaterialPayload + if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { + t.Fatalf("failed unmarshalling tbtc signer material payload: [%v]", err) + } + + if payload.KeyGroup == "" { + t.Fatal("expected non-empty tbtc-signer key group") + } + + if payload.KeyGroupSource == "" { + t.Fatal("expected non-empty tbtc-signer key group source") + } + + legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) + if err != nil { + t.Fatalf("failed decoding legacy private key share payload: [%v]", err) + } + + decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} + if err := decodedPrivateKeyShare.Unmarshal(legacyPrivateKeySharePayload); err != nil { + t.Fatalf("failed unmarshalling decoded private key share: [%v]", err) + } + + actualPayload, err := decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + if !bytes.Equal(actualPayload, legacyEncoded) { t.Fatalf( "unexpected resolved signer payload\nexpected: [%x]\nactual: [%x]", diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native.go b/pkg/tbtc/signer_material_resolver_build_frost_native.go index dca4c73848..3cef396081 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native.go @@ -3,6 +3,9 @@ package tbtc import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" "fmt" frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" @@ -43,13 +46,29 @@ func (btnsmr *buildTaggedNativeSignerMaterialResolver) ResolveSignerMaterial( return nil, fmt.Errorf("private key share is nil") } - payload, err := privateKeyShare.Marshal() + legacyPrivateKeySharePayload, err := privateKeyShare.Marshal() if err != nil { return nil, fmt.Errorf("cannot marshal private key share: [%w]", err) } + walletPublicKeyBytes, err := marshalPublicKey(privateKeyShare.PublicKey()) + if err != nil { + return nil, fmt.Errorf("cannot marshal wallet public key: [%w]", err) + } + + keyGroupDigest := sha256.Sum256(walletPublicKeyBytes) + + payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ + KeyGroup: hex.EncodeToString(keyGroupDigest[:]), + KeyGroupSource: frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + LegacyPrivateKeyShareHex: hex.EncodeToString(legacyPrivateKeySharePayload), + }) + if err != nil { + return nil, fmt.Errorf("cannot marshal tbtc signer material payload: [%w]", err) + } + return &frostsigning.NativeSignerMaterial{ - Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, Payload: payload, }, nil } diff --git a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go index 4138dc0894..45680db2ad 100644 --- a/pkg/tbtc/signer_material_resolver_build_frost_native_test.go +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go @@ -47,55 +47,42 @@ func TestRegisterSignerMaterialResolverForBuild_UsesDefaultProvider( t.Fatalf("failed marshaling expected private key share: [%v]", err) } - var actualPayload []byte - switch nativeSignerMaterial.Format { - case frostsigning.NativeSignerMaterialFormatFrostUniFFIV1: - decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} - if err := decodedPrivateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { - t.Fatalf("failed unmarshalling resolved signer payload: [%v]", err) - } - - actualPayload, err = decodedPrivateKeyShare.Marshal() - if err != nil { - t.Fatalf("failed marshaling decoded private key share: [%v]", err) - } - - case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: - var payload frostsigning.NativeTBTCSignerMaterialPayload - if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { - t.Fatalf("failed unmarshalling tbtc signer material payload: [%v]", err) - } - - if payload.KeyGroup == "" { - t.Fatal("expected non-empty tbtc-signer key group") - } - - if payload.KeyGroupSource == "" { - t.Fatal("expected non-empty tbtc-signer key group source") - } - - legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) - if err != nil { - t.Fatalf("failed decoding legacy private key share payload: [%v]", err) - } - - decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} - if err := decodedPrivateKeyShare.Unmarshal(legacyPrivateKeySharePayload); err != nil { - t.Fatalf("failed unmarshalling decoded private key share: [%v]", err) - } - - actualPayload, err = decodedPrivateKeyShare.Marshal() - if err != nil { - t.Fatalf("failed marshaling decoded private key share: [%v]", err) - } - - default: + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1 { t.Fatalf( - "unexpected native signer material format: [%s]", + "unexpected native signer material format\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, nativeSignerMaterial.Format, ) } + var payload frostsigning.NativeTBTCSignerMaterialPayload + if err := json.Unmarshal(nativeSignerMaterial.Payload, &payload); err != nil { + t.Fatalf("failed unmarshalling tbtc signer material payload: [%v]", err) + } + + if payload.KeyGroup == "" { + t.Fatal("expected non-empty tbtc-signer key group") + } + + if payload.KeyGroupSource == "" { + t.Fatal("expected non-empty tbtc-signer key group source") + } + + legacyPrivateKeySharePayload, err := hex.DecodeString(payload.LegacyPrivateKeyShareHex) + if err != nil { + t.Fatalf("failed decoding legacy private key share payload: [%v]", err) + } + + decodedPrivateKeyShare := &tecdsa.PrivateKeyShare{} + if err := decodedPrivateKeyShare.Unmarshal(legacyPrivateKeySharePayload); err != nil { + t.Fatalf("failed unmarshalling decoded private key share: [%v]", err) + } + + actualPayload, err := decodedPrivateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling decoded private key share: [%v]", err) + } + if !bytes.Equal(expectedPayload, actualPayload) { t.Fatalf( "unexpected resolved signer payload\nexpected: [%x]\nactual: [%x]", From 106642dae7d1f01a8b43d5cec59c3651048c36bb Mon Sep 17 00:00:00 2001 From: maclane Date: Thu, 26 Feb 2026 14:32:03 -0600 Subject: [PATCH 89/96] Remove dead UniFFI legacy registration stubs --- ...ine_uniffi_registration_frost_native_default.go | 2 +- ...gine_uniffi_registration_frost_native_uniffi.go | 7 ------- ...uniffi_registration_frost_native_uniffi_test.go | 14 -------------- 3 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go delete mode 100644 pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go index fa32548b7b..f6156db084 100644 --- a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go @@ -1,4 +1,4 @@ -//go:build frost_native && !(frost_tbtc_signer && cgo) && !(frost_uniffi_sdk && cgo && frost_uniffi_legacy) +//go:build frost_native && !(frost_tbtc_signer && cgo) package signing diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go deleted file mode 100644 index 896fee1a7f..0000000000 --- a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build frost_native && frost_uniffi_sdk && cgo && frost_uniffi_legacy - -package signing - -func registerBuildTaggedNativeFROSTSigningEngine() error { - return nil -} diff --git a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go deleted file mode 100644 index 63b3d2caab..0000000000 --- a/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_uniffi_test.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build frost_native && frost_uniffi_sdk && cgo && frost_uniffi_legacy - -package signing - -import "testing" - -func TestRegisterBuildTaggedNativeFROSTSigningEngine_UniFFILegacyNoop( - t *testing.T, -) { - err := registerBuildTaggedNativeFROSTSigningEngine() - if err != nil { - t.Fatalf("unexpected registration error: [%v]", err) - } -} From b10fd0c7817774dd6deb33c2800aeea3d972d23e Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 27 Feb 2026 11:59:45 -0600 Subject: [PATCH 90/96] frost/signing: enforce attempt coordinator inclusion policy --- ...rimitive_transitional_frost_native_test.go | 81 +++++++++++++++++++ .../native_frost_protocol_frost_native.go | 53 +++++++++--- 2 files changed, 121 insertions(+), 13 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 9874186f3b..27b9812dbc 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -598,6 +598,8 @@ func TestBuildTaggedTBTCSignerRunDKGInputs(t *testing.T) { GroupSize: 5, DishonestThreshold: 2, Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, IncludedMembersIndexes: []group.MemberIndex{1, 3, 5}, }, }, @@ -677,6 +679,81 @@ func TestBuildTaggedTBTCSignerRunDKGInputs_RejectsInvalidRequest(t *testing.T) { } } +func TestIncludedMembersFromRequest_RejectsInvalidAttemptPolicy(t *testing.T) { + testCases := []struct { + name string + request *NativeExecutionFFISigningRequest + errFragment string + }{ + { + name: "zero attempt number", + request: &NativeExecutionFFISigningRequest{ + GroupSize: 3, + Attempt: &Attempt{ + Number: 0, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + }, + errFragment: "attempt number is zero", + }, + { + name: "zero coordinator", + request: &NativeExecutionFFISigningRequest{ + GroupSize: 3, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 0, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + }, + errFragment: "attempt coordinator member index is zero", + }, + { + name: "coordinator not included", + request: &NativeExecutionFFISigningRequest{ + GroupSize: 3, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 3, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + }, + errFragment: "attempt coordinator [3] is not included", + }, + { + name: "member both included and excluded", + request: &NativeExecutionFFISigningRequest{ + GroupSize: 3, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + ExcludedMembersIndexes: []group.MemberIndex{2}, + }, + }, + errFragment: "member [2] is both included and excluded in attempt", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, _, err := includedMembersFromRequest(tc.request) + if err == nil { + t.Fatal("expected error") + } + + if !strings.Contains(err.Error(), tc.errFragment) { + t.Fatalf( + "unexpected error\nexpected to contain: [%v]\nactual: [%v]", + tc.errFragment, + err, + ) + } + }) + } +} + func TestBuildTaggedTBTCSignerSyntheticRoundContributions(t *testing.T) { roundState := &NativeTBTCSignerRoundState{ SessionID: "session-1", @@ -2064,6 +2141,8 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC secondRequest := *baseRequest secondRequest.Attempt = &Attempt{ + Number: 2, + CoordinatorMemberIndex: 1, ExcludedMembersIndexes: []group.MemberIndex{3}, } @@ -2210,6 +2289,8 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC secondRequest := *baseRequest secondRequest.Attempt = &Attempt{ + Number: 2, + CoordinatorMemberIndex: 1, ExcludedMembersIndexes: []group.MemberIndex{2}, } diff --git a/pkg/frost/signing/native_frost_protocol_frost_native.go b/pkg/frost/signing/native_frost_protocol_frost_native.go index 08104e5a96..f0d4f9ee08 100644 --- a/pkg/frost/signing/native_frost_protocol_frost_native.go +++ b/pkg/frost/signing/native_frost_protocol_frost_native.go @@ -432,28 +432,46 @@ func includedMembersFromRequest( return nil, nil, fmt.Errorf("group size must be positive") } + attempt := request.Attempt + if attempt != nil { + if attempt.Number == 0 { + return nil, nil, fmt.Errorf("attempt number is zero") + } + + if attempt.CoordinatorMemberIndex == 0 { + return nil, nil, fmt.Errorf("attempt coordinator member index is zero") + } + } + includedMembersSet := make(map[group.MemberIndex]struct{}) + excludedMembersSet := make(map[group.MemberIndex]struct{}) + + if attempt != nil { + for _, memberIndex := range attempt.ExcludedMembersIndexes { + if memberIndex == 0 { + continue + } + + excludedMembersSet[memberIndex] = struct{}{} + } + } - if request.Attempt != nil && len(request.Attempt.IncludedMembersIndexes) > 0 { - for _, memberIndex := range request.Attempt.IncludedMembersIndexes { + if attempt != nil && len(attempt.IncludedMembersIndexes) > 0 { + for _, memberIndex := range attempt.IncludedMembersIndexes { if memberIndex == 0 { return nil, nil, fmt.Errorf("included member index is zero") } + if _, excluded := excludedMembersSet[memberIndex]; excluded { + return nil, nil, fmt.Errorf( + "member [%v] is both included and excluded in attempt", + memberIndex, + ) + } + includedMembersSet[memberIndex] = struct{}{} } } else { - excludedMembersSet := make(map[group.MemberIndex]struct{}) - if request.Attempt != nil { - for _, memberIndex := range request.Attempt.ExcludedMembersIndexes { - if memberIndex == 0 { - continue - } - - excludedMembersSet[memberIndex] = struct{}{} - } - } - for i := 1; i <= request.GroupSize; i++ { memberIndex := group.MemberIndex(i) if _, excluded := excludedMembersSet[memberIndex]; !excluded { @@ -466,6 +484,15 @@ func includedMembersFromRequest( return nil, nil, fmt.Errorf("included members set is empty") } + if attempt != nil { + if _, included := includedMembersSet[attempt.CoordinatorMemberIndex]; !included { + return nil, nil, fmt.Errorf( + "attempt coordinator [%v] is not included", + attempt.CoordinatorMemberIndex, + ) + } + } + includedMembersIndexes := make([]group.MemberIndex, 0, len(includedMembersSet)) for memberIndex := range includedMembersSet { includedMembersIndexes = append(includedMembersIndexes, memberIndex) From 99294e23d0722f37e14525cd9a1daab11085585b Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 27 Feb 2026 12:37:00 -0600 Subject: [PATCH 91/96] frost/signing: fail closed on invalid coarse attempt policy --- ...ffi_primitive_transitional_frost_native.go | 9 ++ ...rimitive_transitional_frost_native_test.go | 98 +++++++++++++++++++ .../native_frost_protocol_frost_native.go | 35 ++++++- 3 files changed, 137 insertions(+), 5 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 75c20e16ce..9ce5f07646 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "strings" @@ -174,6 +175,14 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) includedMembersSet, includedMembersIndexes, err := includedMembersFromRequest(request) if err != nil { + if errors.Is(err, ErrInvalidSigningAttemptPolicy) { + return nil, fmt.Errorf( + "%w: invalid tbtc-signer signing attempt policy: [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, logger, diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 27b9812dbc..d8b80924f2 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -6,6 +6,7 @@ import ( "bytes" "context" "encoding/hex" + "encoding/json" "errors" "math/big" "reflect" @@ -2355,3 +2356,100 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC ) } } + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_InvalidAttemptPolicy_DoesNotFallback( + t *testing.T, +) { + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + } + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + var observedEvents []NativeTBTCSignerFallbackEvent + err = RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + observedEvents = append(observedEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected observer registration error: [%v]", err) + } + + fixtures, err := tecdsatest.LoadPrivateKeyShareTestFixtures(3) + if err != nil { + t.Fatalf("failed loading key share fixtures: [%v]", err) + } + + privateKeyShare := tecdsa.NewPrivateKeyShare(fixtures[0]) + privateKeySharePayload, err := privateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling private key share: [%v]", err) + } + + signerMaterialPayload, err := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: "group-1", + KeyGroupSource: NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + LegacyPrivateKeyShareHex: hex.EncodeToString(privateKeySharePayload), + }) + if err != nil { + t.Fatalf("cannot marshal signer material payload: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: signerMaterialPayload, + }, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 2, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + ExcludedMembersIndexes: []group.MemberIndex{2}, + }, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected not to include: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if engine.runDKGCalled { + t.Fatal("did not expect RunDKG call for invalid attempt policy") + } + + if len(observedEvents) != 0 { + t.Fatalf( + "did not expect fallback events\nactual: [%v]", + observedEvents, + ) + } +} diff --git a/pkg/frost/signing/native_frost_protocol_frost_native.go b/pkg/frost/signing/native_frost_protocol_frost_native.go index f0d4f9ee08..6a3189461a 100644 --- a/pkg/frost/signing/native_frost_protocol_frost_native.go +++ b/pkg/frost/signing/native_frost_protocol_frost_native.go @@ -5,6 +5,7 @@ package signing import ( "context" "encoding/json" + "errors" "fmt" "sort" @@ -16,6 +17,12 @@ import ( const nativeFROSTMessageTypePrefix = "frost_signing/native_frost/" +var ( + // ErrInvalidSigningAttemptPolicy indicates the provided attempt metadata + // violates coordinator/cohort policy invariants. + ErrInvalidSigningAttemptPolicy = errors.New("invalid signing attempt policy") +) + type nativeFROSTUniFFIV2SignerMaterial struct { KeyPackage *NativeFROSTKeyPackage `json:"keyPackage"` PublicKeyPackage *NativeFROSTPublicKeyPackage `json:"publicKeyPackage"` @@ -435,11 +442,17 @@ func includedMembersFromRequest( attempt := request.Attempt if attempt != nil { if attempt.Number == 0 { - return nil, nil, fmt.Errorf("attempt number is zero") + return nil, nil, fmt.Errorf( + "%w: attempt number is zero", + ErrInvalidSigningAttemptPolicy, + ) } if attempt.CoordinatorMemberIndex == 0 { - return nil, nil, fmt.Errorf("attempt coordinator member index is zero") + return nil, nil, fmt.Errorf( + "%w: attempt coordinator member index is zero", + ErrInvalidSigningAttemptPolicy, + ) } } @@ -459,12 +472,16 @@ func includedMembersFromRequest( if attempt != nil && len(attempt.IncludedMembersIndexes) > 0 { for _, memberIndex := range attempt.IncludedMembersIndexes { if memberIndex == 0 { - return nil, nil, fmt.Errorf("included member index is zero") + return nil, nil, fmt.Errorf( + "%w: included member index is zero", + ErrInvalidSigningAttemptPolicy, + ) } if _, excluded := excludedMembersSet[memberIndex]; excluded { return nil, nil, fmt.Errorf( - "member [%v] is both included and excluded in attempt", + "%w: member [%v] is both included and excluded in attempt", + ErrInvalidSigningAttemptPolicy, memberIndex, ) } @@ -481,13 +498,21 @@ func includedMembersFromRequest( } if len(includedMembersSet) == 0 { + if attempt != nil { + return nil, nil, fmt.Errorf( + "%w: included members set is empty", + ErrInvalidSigningAttemptPolicy, + ) + } + return nil, nil, fmt.Errorf("included members set is empty") } if attempt != nil { if _, included := includedMembersSet[attempt.CoordinatorMemberIndex]; !included { return nil, nil, fmt.Errorf( - "attempt coordinator [%v] is not included", + "%w: attempt coordinator [%v] is not included", + ErrInvalidSigningAttemptPolicy, attempt.CoordinatorMemberIndex, ) } From b19e57cca666ee31a31489f73ac3bcfbe6351072 Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 27 Feb 2026 12:43:52 -0600 Subject: [PATCH 92/96] frost/signing: add coarse attempt-policy error matrix coverage --- ...ffi_primitive_transitional_frost_native.go | 2 +- ...rimitive_transitional_frost_native_test.go | 182 ++++++++++++------ 2 files changed, 119 insertions(+), 65 deletions(-) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 9ce5f07646..38027086ca 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -177,7 +177,7 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) if err != nil { if errors.Is(err, ErrInvalidSigningAttemptPolicy) { return nil, fmt.Errorf( - "%w: invalid tbtc-signer signing attempt policy: [%v]", + "%w: invalid tbtc-signer signing attempt policy: %w", ErrNativeBridgeOperationFailed, err, ) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index d8b80924f2..1f737b3164 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -2360,29 +2360,6 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_InvalidAttemptPolicy_DoesNotFallback( t *testing.T, ) { - engine := &mockBuildTaggedTBTCSignerEngine{ - version: "tbtc-signer/0.1.0-bootstrap", - } - UnregisterNativeTBTCSignerEngine() - UnregisterNativeTBTCSignerFallbackObserver() - t.Cleanup(UnregisterNativeTBTCSignerEngine) - t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) - - err := RegisterNativeTBTCSignerEngine(engine) - if err != nil { - t.Fatalf("unexpected registration error: [%v]", err) - } - - var observedEvents []NativeTBTCSignerFallbackEvent - err = RegisterNativeTBTCSignerFallbackObserver( - func(event NativeTBTCSignerFallbackEvent) { - observedEvents = append(observedEvents, event) - }, - ) - if err != nil { - t.Fatalf("unexpected observer registration error: [%v]", err) - } - fixtures, err := tecdsatest.LoadPrivateKeyShareTestFixtures(3) if err != nil { t.Fatalf("failed loading key share fixtures: [%v]", err) @@ -2403,53 +2380,130 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC t.Fatalf("cannot marshal signer material payload: [%v]", err) } - primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} - - _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ - Message: big.NewInt(123), - SessionID: "session-1", - MemberIndex: 1, - GroupSize: 3, - DishonestThreshold: 1, - SignerMaterial: &NativeSignerMaterial{ - Format: NativeSignerMaterialFormatFrostTBTCSignerV1, - Payload: signerMaterialPayload, + testCases := []struct { + name string + attempt *Attempt + }{ + { + name: "zero attempt number", + attempt: &Attempt{ + Number: 0, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, }, - Attempt: &Attempt{ - Number: 1, - CoordinatorMemberIndex: 2, - IncludedMembersIndexes: []group.MemberIndex{1, 2}, - ExcludedMembersIndexes: []group.MemberIndex{2}, + { + name: "zero coordinator", + attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 0, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + }, + { + name: "coordinator not included", + attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 3, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + }, + { + name: "included members empty after exclusions", + attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + ExcludedMembersIndexes: []group.MemberIndex{1, 2, 3}, + }, + }, + { + name: "member included and excluded", + attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 2, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + ExcludedMembersIndexes: []group.MemberIndex{2}, + }, }, - }) - if err == nil { - t.Fatal("expected error") } - if !errors.Is(err, ErrNativeBridgeOperationFailed) { - t.Fatalf( - "unexpected error\nexpected: [%v]\nactual: [%v]", - ErrNativeBridgeOperationFailed, - err, - ) - } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + } + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) - if errors.Is(err, ErrNativeCryptographyUnavailable) { - t.Fatalf( - "unexpected error\nexpected not to include: [%v]\nactual: [%v]", - ErrNativeCryptographyUnavailable, - err, - ) - } + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } - if engine.runDKGCalled { - t.Fatal("did not expect RunDKG call for invalid attempt policy") - } + var observedEvents []NativeTBTCSignerFallbackEvent + err = RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + observedEvents = append(observedEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected observer registration error: [%v]", err) + } - if len(observedEvents) != 0 { - t.Fatalf( - "did not expect fallback events\nactual: [%v]", - observedEvents, - ) + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: signerMaterialPayload, + }, + Attempt: tc.attempt, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if !errors.Is(err, ErrInvalidSigningAttemptPolicy) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrInvalidSigningAttemptPolicy, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected not to include: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if engine.runDKGCalled { + t.Fatal("did not expect RunDKG call for invalid attempt policy") + } + + if len(observedEvents) != 0 { + t.Fatalf( + "did not expect fallback events\nactual: [%v]", + observedEvents, + ) + } + }) } } From 1415e04a268d6d760d32ab747d86195441ec513d Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 27 Feb 2026 14:00:25 -0600 Subject: [PATCH 93/96] fix: address gosec G118 context cancellation findings --- pkg/generator/scheduler.go | 6 +++++- pkg/tbtc/dkg_loop.go | 1 + pkg/tbtc/node.go | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/generator/scheduler.go b/pkg/generator/scheduler.go index 73c9d25350..328d046bd9 100644 --- a/pkg/generator/scheduler.go +++ b/pkg/generator/scheduler.go @@ -113,9 +113,13 @@ func (s *Scheduler) resume() { // workMutex is locked. func (s *Scheduler) startWorker(workerFn func(context.Context)) { ctx, cancelFn := context.WithCancel(context.Background()) - s.stops = append(s.stops, cancelFn) + s.stops = append(s.stops, func() { + cancelFn() + }) go func() { + defer cancelFn() + for { select { case <-ctx.Done(): diff --git a/pkg/tbtc/dkg_loop.go b/pkg/tbtc/dkg_loop.go index 4b7955abc9..bcd02e02a9 100644 --- a/pkg/tbtc/dkg_loop.go +++ b/pkg/tbtc/dkg_loop.go @@ -199,6 +199,7 @@ func (drl *dkgRetryLoop) start( drl.memberIndex, fmt.Sprintf("%v-%v", drl.seed, drl.attemptCounter), ) + cancelAnnounceCtx() if err != nil { drl.logger.Warnf( "[member:%v] announcement for attempt [%v] "+ diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 3b3f6283d9..03801a4e72 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -1518,6 +1518,8 @@ func withCancelOnBlock( block uint64, waitForBlockFn waitForBlockFn, ) (context.Context, context.CancelFunc) { + // #nosec G118 -- The returned cancel function is intentionally propagated + // to the caller and also invoked by the helper goroutine below. blockCtx, cancelBlockCtx := context.WithCancel(ctx) go func() { From 7a0b24aa97bdecbfd7e339e3ba0d8ad23be1907c Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 27 Feb 2026 14:13:28 -0600 Subject: [PATCH 94/96] fix: annotate scheduler cancel lifecycle for gosec --- pkg/generator/scheduler.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/generator/scheduler.go b/pkg/generator/scheduler.go index 328d046bd9..014f7b1395 100644 --- a/pkg/generator/scheduler.go +++ b/pkg/generator/scheduler.go @@ -112,6 +112,8 @@ func (s *Scheduler) resume() { // This function should be executed only be the Scheduler and when the // workMutex is locked. func (s *Scheduler) startWorker(workerFn func(context.Context)) { + // #nosec G118 -- The cancel function is retained in s.stops and invoked + // when the scheduler stops workers. ctx, cancelFn := context.WithCancel(context.Background()) s.stops = append(s.stops, func() { cancelFn() From d1eaa112312f1208e81c8f2f71cb34da85b86c7c Mon Sep 17 00:00:00 2001 From: maclane Date: Fri, 27 Feb 2026 16:40:17 -0600 Subject: [PATCH 95/96] frost-signing: fail-close consumed attempt replay errors --- ...ffi_primitive_transitional_frost_native.go | 20 +++ ...rimitive_transitional_frost_native_test.go | 114 ++++++++++++++++++ .../native_frost_protocol_frost_native.go | 3 + 3 files changed, 137 insertions(+) diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go index 38027086ca..e38b12eecb 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -42,6 +42,7 @@ const buildTaggedTBTCSignerVersionPrefix = "tbtc-signer/" const buildTaggedTBTCSignerBootstrapVersionPrerelease = "bootstrap" const buildTaggedTBTCSignerSyntheticContributionDomain = "tbtc-signer-bootstrap-contribution-v1" const buildTaggedTBTCSignerMessageTypePrefix = "frost_signing/native_tbtc_signer/" +const buildTaggedTBTCSignerConsumedAttemptReplayErrorFragment = "already consumed for sign attempt" type nativeTBTCSignerVersionedEngine interface { Version() (string, error) @@ -320,6 +321,15 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) includedMembersIndexes, ) if err != nil { + if isBuildTaggedTBTCSignerConsumedAttemptReplayError(err) { + return nil, fmt.Errorf( + "%w: consumed tbtc-signer attempt replay: %w: %v", + ErrNativeBridgeOperationFailed, + ErrConsumedSigningAttemptReplay, + err, + ) + } + return btlcnnefsp.fallbackTBTCSignerLegacySigning( ctx, logger, @@ -359,6 +369,16 @@ func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) return coarseSignature, nil } +func isBuildTaggedTBTCSignerConsumedAttemptReplayError(err error) bool { + if err == nil { + return false + } + + message := strings.ToLower(err.Error()) + return strings.Contains(message, "attempt_id") && + strings.Contains(message, buildTaggedTBTCSignerConsumedAttemptReplayErrorFragment) +} + func buildTaggedTBTCSignerRunDKGInputs( request *NativeExecutionFFISigningRequest, ) ([]NativeTBTCSignerDKGParticipant, uint16, error) { diff --git a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go index 1f737b3164..03a01951cc 100644 --- a/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -2507,3 +2507,117 @@ func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTC }) } } + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_ConsumedAttemptReplay_DoesNotFallback( + t *testing.T, +) { + fixtures, err := tecdsatest.LoadPrivateKeyShareTestFixtures(3) + if err != nil { + t.Fatalf("failed loading key share fixtures: [%v]", err) + } + + privateKeyShare := tecdsa.NewPrivateKeyShare(fixtures[0]) + privateKeySharePayload, err := privateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling private key share: [%v]", err) + } + + signerMaterialPayload, err := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: "group-1", + KeyGroupSource: NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + LegacyPrivateKeyShareHex: hex.EncodeToString(privateKeySharePayload), + }) + if err != nil { + t.Fatalf("cannot marshal signer material payload: [%v]", err) + } + + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + startErr: errors.New( + "validation: attempt_id [11] already consumed for sign attempt in session [session-1]", + ), + } + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + + err = RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + var observedEvents []NativeTBTCSignerFallbackEvent + err = RegisterNativeTBTCSignerFallbackObserver( + func(event NativeTBTCSignerFallbackEvent) { + observedEvents = append(observedEvents, event) + }, + ) + if err != nil { + t.Fatalf("unexpected observer registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err = primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: signerMaterialPayload, + }, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2}, + }, + }) + if err == nil { + t.Fatal("expected error") + } + + if !errors.Is(err, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if !errors.Is(err, ErrConsumedSigningAttemptReplay) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrConsumedSigningAttemptReplay, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unexpected error\nexpected not to include: [%v]\nactual: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if !engine.runDKGCalled { + t.Fatal("expected RunDKG call before consumed-attempt replay rejection") + } + + if len(observedEvents) != 0 { + t.Fatalf( + "did not expect fallback events\nactual: [%v]", + observedEvents, + ) + } + + if !strings.Contains(err.Error(), "already consumed for sign attempt") { + t.Fatalf( + "expected replay fragment in error message\nactual: [%v]", + err, + ) + } +} diff --git a/pkg/frost/signing/native_frost_protocol_frost_native.go b/pkg/frost/signing/native_frost_protocol_frost_native.go index 6a3189461a..e2c496be73 100644 --- a/pkg/frost/signing/native_frost_protocol_frost_native.go +++ b/pkg/frost/signing/native_frost_protocol_frost_native.go @@ -21,6 +21,9 @@ var ( // ErrInvalidSigningAttemptPolicy indicates the provided attempt metadata // violates coordinator/cohort policy invariants. ErrInvalidSigningAttemptPolicy = errors.New("invalid signing attempt policy") + // ErrConsumedSigningAttemptReplay indicates signer-side replay protection + // rejected a previously consumed signing attempt payload. + ErrConsumedSigningAttemptReplay = errors.New("consumed signing attempt replay") ) type nativeFROSTUniFFIV2SignerMaterial struct { From c3d68330f6b299a1a35853b572076210e651a656 Mon Sep 17 00:00:00 2001 From: maclane Date: Sat, 28 Feb 2026 20:49:01 -0600 Subject: [PATCH 96/96] test(frost): add Gemini audit coverage for ffi error payloads --- ...c_signer_registration_frost_native_test.go | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go index 3e49e0b529..941688275a 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -147,6 +147,56 @@ func TestBuildTaggedTBTCSignerResultStatusError_BridgeOperationFailure(t *testin } } +func TestBuildTaggedTBTCSignerResultStatusError_BridgeOperationFailure_InvalidPayload( + t *testing.T, +) { + err := buildTaggedTBTCSignerResultStatusError( + "BuildTaprootTx", + 2, + []byte("{invalid-json"), + ) + if err == nil { + t.Fatal("expected bridge operation failure error") + } + + if !errors.Is(err, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "expected native bridge operation failed error: [%v], got [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if !strings.Contains(err.Error(), "cannot decode error payload") { + t.Fatalf("unexpected bridge operation error: [%v]", err) + } +} + +func TestBuildTaggedTBTCSignerResultStatusError_BridgeOperationFailure_FallbackPayload( + t *testing.T, +) { + err := buildTaggedTBTCSignerResultStatusError( + "BuildTaprootTx", + 2, + []byte(`{"code":"internal_error","message":"failed to encode error"}`), + ) + if err == nil { + t.Fatal("expected bridge operation failure error") + } + + if !errors.Is(err, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "expected native bridge operation failed error: [%v], got [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if !strings.Contains(err.Error(), "internal_error: failed to encode error") { + t.Fatalf("unexpected bridge operation error: [%v]", err) + } +} + func TestBuildTaggedTBTCSignerRunDKGRequestPayload(t *testing.T) { payload, err := buildTaggedTBTCSignerRunDKGRequestPayload( "session-1",