diff --git a/.dockerignore b/.dockerignore index 9c48e7b076..e46b29ee0f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -31,6 +31,11 @@ token-tracker/ **/gen/**/*.go !**/gen/gen.go !**/gen/cmd/cmd.go +!pkg/chain/ethereum/frost/gen/abi/*.go +!pkg/chain/ethereum/frost/gen/validatorabi/*.go +!pkg/chain/ethereum/tbtc/gen/abi/*.go +!pkg/chain/ethereum/tbtc/gen/contract/*.go +!pkg/chain/ethereum/tbtc/gen/cmd/*.go # Legacy V1 contracts bindings. # We won't generate new bindings in the docker build process, but use the existing ones. diff --git a/cmd/flags.go b/cmd/flags.go index 6ce094c2e6..289e5742b3 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -308,6 +308,29 @@ 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). "+ + "`native` allows transitional legacy fallback; `ffi` requires native execution. "+ + "Empty value selects legacy.", + ) + + cmd.Flags().BoolVar( + &cfg.Tbtc.DisableLegacyECDSA, + "tbtc.disableLegacyECDSA", + false, + "Disable legacy ECDSA wallet DKG and pre-params generation for FROST-only deployments.", + ) + + cmd.Flags().BoolVar( + &cfg.Tbtc.DisableLegacySortitionPoolMonitoring, + "tbtc.disableLegacySortitionPoolMonitoring", + false, + "Disable legacy ECDSA sortition pool monitoring for FROST-only deployments.", + ) } // Initialize flags for Maintainer configuration. @@ -385,5 +408,7 @@ func initDeveloperFlags(command *cobra.Command) { initContractAddressFlag(chainEthereum.RandomBeaconContractName) initContractAddressFlag(chainEthereum.TokenStakingContractName) initContractAddressFlag(chainEthereum.WalletRegistryContractName) + initContractAddressFlag(chainEthereum.FrostWalletRegistryContractName) + initContractAddressFlag(chainEthereum.FrostDkgValidatorContractName) initContractAddressFlag(chainEthereum.WalletProposalValidatorContractName) } diff --git a/cmd/flags_test.go b/cmd/flags_test.go index 58ee1249ae..a59b65555c 100644 --- a/cmd/flags_test.go +++ b/cmd/flags_test.go @@ -225,6 +225,31 @@ 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: "", + }, + "tbtc.disableLegacyECDSA": { + readValueFunc: func(c *config.Config) interface{} { + return c.Tbtc.DisableLegacyECDSA + }, + flagName: "--tbtc.disableLegacyECDSA", + flagValue: "", // don't provide any value + expectedValueFromFlag: true, + defaultValue: false, + }, + "tbtc.disableLegacySortitionPoolMonitoring": { + readValueFunc: func(c *config.Config) interface{} { + return c.Tbtc.DisableLegacySortitionPoolMonitoring + }, + flagName: "--tbtc.disableLegacySortitionPoolMonitoring", + flagValue: "", // don't provide any value + expectedValueFromFlag: true, + defaultValue: false, + }, "maintainer.bitcoinDifficulty": { readValueFunc: func(c *config.Config) interface{} { return c.Maintainer.BitcoinDifficulty.Enabled }, flagName: "--bitcoinDifficulty", diff --git a/cmd/helpers.go b/cmd/helpers.go index ee3fa35416..44a15d8ae1 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -98,6 +98,8 @@ func buildContractAddresses(lineLength int, prefix, suffix string, ethereumConfi chainEthereum.RandomBeaconContractName, chainEthereum.BridgeContractName, chainEthereum.WalletRegistryContractName, + chainEthereum.FrostWalletRegistryContractName, + chainEthereum.FrostDkgValidatorContractName, chainEthereum.TokenStakingContractName, chainEthereum.WalletProposalValidatorContractName, } diff --git a/config/config_test.go b/config/config_test.go index 26d8a74fea..781d97e59d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -16,6 +16,7 @@ import ( "github.com/keep-network/keep-core/pkg/chain/ethereum" ethereumBeacon "github.com/keep-network/keep-core/pkg/chain/ethereum/beacon/gen" ethereumEcdsa "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen" + ethereumFrost "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen" ethereumTbtc "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen" ethereumThreshold "github.com/keep-network/keep-core/pkg/chain/ethereum/threshold/gen" ) @@ -68,6 +69,8 @@ func TestReadConfigFromFile(t *testing.T) { expectedValue: map[string]string{ "randombeacon": "0xcf64c2a367341170cb4e09cf8c0ed137d8473ceb", "walletregistry": "0x143ba24e66fce8bca22f7d739f9a932c519b1c76", + "frostwalletregistry": "0x0000000000000000000000000000000000000000", + "frostdkgvalidator": "0x0000000000000000000000000000000000000000", "tokenstaking": "0xa363a197f1bbb8877f50350234e3f15fb4175457", "bridge": "0x138D2a0c87BA9f6BE1DCc13D6224A6aCE9B6b6F0", "maintainerproxy": "0xC6D21c2871586A2B098c0ad043fF0D47a3c7e7ae", @@ -331,6 +334,8 @@ func TestReadConfig_ReadContracts(t *testing.T) { ethereumBeacon.RandomBeaconAddress = "0xd1640b381327c2d5425d6d3d605539a3db72f857" ethereumEcdsa.WalletRegistryAddress = "0xdb3dd6d4f43d39c996d0afeb6fbabc284f9ffb1a" + ethereumFrost.FrostWalletRegistryAddress = "0x0000000000000000000000000000000000000000" + ethereumFrost.FrostDkgValidatorAddress = "0x0000000000000000000000000000000000000000" ethereumThreshold.TokenStakingAddress = "0xaa7b41039ea8f9ec2d89bbe96e19f97b6c267a27" ethereumTbtc.BridgeAddress = "0x9490165195503fcf6a0fd20ac113223fefb66ed5" ethereumTbtc.WalletProposalValidatorAddress = "0xE7d33d8AA55B73a93059a24b900366894684a497" @@ -340,6 +345,8 @@ func TestReadConfig_ReadContracts(t *testing.T) { expectedRandomBeaconAddress string expectedWalletRegistryAddress string + expectedFrostWalletRegistryAddress string + expectedFrostDkgValidatorAddress string expectedTokenStakingAddress string expectedBridgeAddress string expectedWalletProposalValidatorAddress string @@ -348,6 +355,8 @@ func TestReadConfig_ReadContracts(t *testing.T) { configFilePath: "../test/config_no_contracts.toml", expectedRandomBeaconAddress: "0xd1640b381327c2d5425d6d3d605539a3db72f857", expectedWalletRegistryAddress: "0xdb3dd6d4f43d39c996d0afeb6fbabc284f9ffb1a", + expectedFrostWalletRegistryAddress: "0x0000000000000000000000000000000000000000", + expectedFrostDkgValidatorAddress: "0x0000000000000000000000000000000000000000", expectedTokenStakingAddress: "0xaa7b41039ea8f9ec2d89bbe96e19f97b6c267a27", expectedBridgeAddress: "0x9490165195503fcf6a0fd20ac113223fefb66ed5", expectedWalletProposalValidatorAddress: "0xE7d33d8AA55B73a93059a24b900366894684a497", @@ -356,6 +365,8 @@ func TestReadConfig_ReadContracts(t *testing.T) { configFilePath: "../test/config.toml", expectedRandomBeaconAddress: "0xcf64c2a367341170cb4e09cf8c0ed137d8473ceb", expectedWalletRegistryAddress: "0x143ba24e66fce8bca22f7d739f9a932c519b1c76", + expectedFrostWalletRegistryAddress: "0x0000000000000000000000000000000000000000", + expectedFrostDkgValidatorAddress: "0x0000000000000000000000000000000000000000", expectedTokenStakingAddress: "0xa363a197f1bbb8877f50350234e3f15fb4175457", expectedBridgeAddress: "0x138D2a0c87BA9f6BE1DCc13D6224A6aCE9B6b6F0", expectedWalletProposalValidatorAddress: "0xfdc315b0e608b7cDE9166D9D69a1506779e3E0CA", @@ -364,6 +375,8 @@ func TestReadConfig_ReadContracts(t *testing.T) { configFilePath: "../test/config_mixed_contracts.toml", expectedRandomBeaconAddress: "0xd1640b381327c2d5425d6d3d605539a3db72f857", expectedWalletRegistryAddress: "0x143ba24e66fce8bca22f7d739f9a932c519b1c76", + expectedFrostWalletRegistryAddress: "0x0000000000000000000000000000000000000000", + expectedFrostDkgValidatorAddress: "0x0000000000000000000000000000000000000000", expectedTokenStakingAddress: "0xaa7b41039ea8f9ec2d89bbe96e19f97b6c267a27", expectedBridgeAddress: "0x9490165195503fcf6a0fd20ac113223fefb66ed5", expectedWalletProposalValidatorAddress: "0xE7d33d8AA55B73a93059a24b900366894684a497", @@ -398,6 +411,14 @@ func TestReadConfig_ReadContracts(t *testing.T) { validate(ethereum.RandomBeaconContractName, test.expectedRandomBeaconAddress) validate(ethereum.WalletRegistryContractName, test.expectedWalletRegistryAddress) + validate( + ethereum.FrostWalletRegistryContractName, + test.expectedFrostWalletRegistryAddress, + ) + validate( + ethereum.FrostDkgValidatorContractName, + test.expectedFrostDkgValidatorAddress, + ) validate(ethereum.TokenStakingContractName, test.expectedTokenStakingAddress) validate(ethereum.BridgeContractName, test.expectedBridgeAddress) validate(ethereum.WalletProposalValidatorContractName, test.expectedWalletProposalValidatorAddress) diff --git a/config/contracts.go b/config/contracts.go index 9bd5055020..b7376c32cb 100644 --- a/config/contracts.go +++ b/config/contracts.go @@ -12,6 +12,7 @@ import ( ethereumBeacon "github.com/keep-network/keep-core/pkg/chain/ethereum/beacon/gen" ethereumEcdsa "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen" + ethereumFrost "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen" ethereumTbtc "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen" ethereumThreshold "github.com/keep-network/keep-core/pkg/chain/ethereum/threshold/gen" ) @@ -39,6 +40,8 @@ func initializeContractAddressesAliases() { aliasEthereumContract(chainEthereum.RandomBeaconContractName) aliasEthereumContract(chainEthereum.TokenStakingContractName) aliasEthereumContract(chainEthereum.WalletRegistryContractName) + aliasEthereumContract(chainEthereum.FrostWalletRegistryContractName) + aliasEthereumContract(chainEthereum.FrostDkgValidatorContractName) aliasEthereumContract(chainEthereum.BridgeContractName) aliasEthereumContract(chainEthereum.MaintainerProxyContractName) aliasEthereumContract(chainEthereum.LightRelayContractName) @@ -72,6 +75,14 @@ func (c *Config) resolveContractsAddresses() { chainEthereum.WalletRegistryContractName, ethereumEcdsa.WalletRegistryAddress, ) + resolveContractAddress( + chainEthereum.FrostWalletRegistryContractName, + ethereumFrost.FrostWalletRegistryAddress, + ) + resolveContractAddress( + chainEthereum.FrostDkgValidatorContractName, + ethereumFrost.FrostDkgValidatorAddress, + ) resolveContractAddress( chainEthereum.BridgeContractName, ethereumTbtc.BridgeAddress, diff --git a/docs/development/frost-roast-retry-rollout.adoc b/docs/development/frost-roast-retry-rollout.adoc new file mode 100644 index 0000000000..ee920a555d --- /dev/null +++ b/docs/development/frost-roast-retry-rollout.adoc @@ -0,0 +1,146 @@ += FROST/ROAST Retry Rollout Guide + +*Author:* Threshold Labs +*Status:* Draft +*Date:* 2026-05-23 + +== Summary + +This document describes the operational lifecycle of the +ROAST-driven retry path introduced by RFC-21 +(`docs/rfc/rfc-21-roast-coordinator-retry-and-transition-evidence.adoc`) +and implemented across Phases 1-7 of that RFC. It is intended for +node operators and release engineers planning a rollout of the new +retry semantics. + +The feature ships as a build-tagged code path. A production +binary built without the tag contains *no ROAST retry code*; +every signing flow uses the pre-RFC-21 legacy retry shuffle. A +binary built with the tag still executes the legacy path unless +the operator explicitly opts in via an environment variable, and +even then the new path silently falls back to legacy whenever its +preconditions are not met. + +== Activation prerequisites + +All three must be true at the same time for the ROAST retry path +to influence participant selection on a given session attempt: + +. *Build tag set.* The keep-core binary is built with + `-tags frost_roast_retry`. Without the tag, the dispatcher + package does not include the ROAST selector at all. +. *Operator opt-in env var.* The runtime environment defines + `KEEP_CORE_FROST_ROAST_RETRY_ENABLED=true` (case-insensitive, + whitespace-trimmed). The variable is read per call (not + cached), so an operator can flip the switch during a debugging + session without restarting the node. +. *Coordinator registered.* A caller has invoked + `signing.RegisterRoastRetryCoordinator(deps)` at process + startup with the node's operator-key signer, the network's + signature verifier, and the node's member index. + +When any of these is missing, the receive loops, executor +adapter, and signing-loop selector all behave as in the legacy +pre-RFC-21 path. The behavioural rollback is therefore *configuration- +only*: toggle the env var off and the next signing attempt uses +the legacy retry shuffle. + +== Behavioural matrix + +[options="header"] +|=== +| Build tag | Env var | Registry | Bundle present | Behaviour +| not set | _any_ | _any_ | _any_ | Legacy retry shuffle +| set | unset | _any_ | _any_ | Legacy retry shuffle (env-var gate) +| set | true | empty | _any_ | Legacy retry shuffle (no coordinator) +| set | true | populated | absent | Legacy retry shuffle (first attempt / no transition yet) +| set | true | populated | present | ROAST `EvaluateRoastRetryForSigning` +|=== + +The bundle is "present" once the elected coordinator's node has +produced a `TransitionMessage` at the end of a prior attempt +(see Phase 7.1 in RFC-21). Until that happens, the ROAST path is +dormant and the legacy path provides liveness. + +== Error handling discipline + +The orchestration layer distinguishes two error classes: + +* *Static-configuration errors.* Env var unset, no coordinator + registered, signer-material format not extractable. These are + deterministic per deployment configuration: every honest signer + observes the same outcome. Logged at INFO, signing flow + continues with the legacy retry shuffle. + +* *Runtime state-machine errors.* `Coordinator.BeginAttempt` + failures, internal invariant violations, + `ErrAttemptInfeasible` from the policy's threshold floor. These + are non-deterministic across nodes. Treated as *hard failures*: + the session is declared failed and the operator is notified + via the standard signing-failure log path. Falling back to + legacy on these errors would let one node use legacy retry + while another uses ROAST, which would split the signing group + on `NextAttempt` agreement. + +This discipline is the load-bearing safety property of the +RFC-21 design and is enforced in +`pkg/frost/signing/roast_retry_executor_entry_frost_native.go`. + +== Production rollout sequencing + +. *Build the binary with the tag.* Internal builds and CI + pipelines already exercise the tag via + `go test -tags 'frost_roast_retry' ./pkg/frost/... ./pkg/tbtc/...`. + Production binaries adopt the tag once the readiness manifest + in the cross-repo tBTC monorepo's `docs/operations/` directory + flips to `present`. +. *Verify FROST/UniFFI V1 migration.* The DKG-pubkey extraction + helper rejects FrostUniFFIV1 signer material. The Phase 7 + manifest flip is gated on verified migration off V1 across + production signers; until that migration completes, ROAST + retry would silently fall back to legacy on V1-bearing nodes. +. *Stage operator opt-in.* Operators set + `KEEP_CORE_FROST_ROAST_RETRY_ENABLED=true` on a subset of nodes + first. Static-configuration fallback guarantees mixed-state + deployments stay correct: nodes without the env var simply use + legacy. Beware: a node with the env var set but no registered + coordinator (e.g., due to a misconfigured startup script) still + uses legacy, so the safety property holds. +. *Monitor for runtime hard-failures.* The "ROAST orchestration" + log lines under + `keep-frost-roast-orchestration` and + `keep-frost-roast-retry` loggers indicate transitions of the + new state machine. A spike in WARN/ERROR entries from these + loggers is the early signal of trouble. +. *Roll back via env var.* If anything misbehaves, unset + `KEEP_CORE_FROST_ROAST_RETRY_ENABLED` and restart (or wait for + the per-call check to flip the next attempt). The legacy code + paths are retained through Phase 6 and 7 deliberately to make + this rollback bit-for-bit safe. + +== FROST DKG digest format coupling + +The FROST DKG result digest is a cross-repo wire-format contract +shared by keep-core, the tBTC TypeScript test vectors, and the +on-chain `FrostDkgValidator.resultDigest(...)` implementation. +The current tag is `tbtc-frost-dkg-result-v1`. + +Treat that string, the ABI field order, and the field types as a +single coupled interface. A future wire-format migration, for +example changing the digest fields or their order, must update +keep-core, the tBTC fixture/emitter, and the on-chain validator +together. Do not change the string on one side as an isolated +cleanup. + +== Cross-references + +* RFC-21: `docs/rfc/rfc-21-roast-coordinator-retry-and-transition-evidence.adoc` +* Build-tag scaffolding: `pkg/frost/signing/roast_retry_registration_*.go` +* Orchestration entry point: `pkg/frost/signing/roast_retry_orchestration.go` +* Executor-adapter wiring: `pkg/frost/signing/native_ffi_executor_adapter.go` +* Signing-loop dispatcher: `pkg/tbtc/signing_loop_roast_dispatcher.go` +* ROAST-driven selector: `pkg/tbtc/signing_loop_selector_frost_roast_retry.go` +* Bundle registry: `pkg/frost/signing/roast_retry_bundle_registry_*.go` +* Readiness env var: `pkg/frost/signing/roast_retry_readiness.go` +* Coordinator state machine: `pkg/frost/roast/coordinator_state.go` +* Adapter type: `pkg/frost/roast/signing_retry_adapter.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..7120c2d9c9 --- /dev/null +++ b/docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc @@ -0,0 +1,49 @@ += RFC-20: Schnorr/FROST Migration Scaffold + +*Author:* Threshold Labs +*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/docs/rfc/rfc-21-roast-coordinator-retry-and-transition-evidence.adoc b/docs/rfc/rfc-21-roast-coordinator-retry-and-transition-evidence.adoc new file mode 100644 index 0000000000..f1ddcd5a73 --- /dev/null +++ b/docs/rfc/rfc-21-roast-coordinator-retry-and-transition-evidence.adoc @@ -0,0 +1,752 @@ += RFC-21: ROAST Coordinator, Retry, and Transition Evidence + +*Author:* Threshold Labs +*Status:* Draft +*Date:* 2026-05-22 + +== Summary + +This RFC defines the protocol layer that lets keep-core honestly advertise its +FROST signing path as ROAST-compliant. Today the package layout names ROAST +concepts (`pkg/frost/roast`, `pkg/frost/retry`) but the actual semantics fall +short of the protocol in two specific places: + +* The retry policy in `pkg/frost/retry/retry.go` is byte-identical to the + tECDSA shuffle in `pkg/tecdsa/retry/retry.go`. It is a deterministic + participant shuffle, not ROAST-aware attempt advancement. +* The three FFI/native-FROST receive loops in `pkg/frost/signing/` drop + channel overflows with `select { default }`, with no bounded transition + evidence and no retransmission contract. + +This RFC proposes a layered design that closes both gaps together, because +they share the same notion of *attempt context* and *transition evidence*. +It is broken into discrete PR-sized phases so the migration can land +incrementally without regressing the existing signing flow. + +== Motivation + +The ROAST paper (Ruffing-Ronge-Aranha-Schneider, CCS 2022) describes a +coordinator-driven retry protocol that turns FROST's brittle round +synchronisation into an asynchronous robust signing primitive. The key +invariants are: + +1. *Attempt context.* Every signing attempt is bound to a deterministic + context (session, key group, message digest, attempt counter, included + participant set). All in-flight protocol messages must reference the + attempt context they belong to. Messages for a stale or future attempt + must not influence the current attempt's transcript. +2. *Transition evidence.* When the coordinator moves an attempt forward it + must publish (or be able to publish on demand) evidence that justifies + the transition: which contributions arrived, which were rejected, which + peers failed to respond within the attempt's bound, and what new + exclusion set the next attempt should use. This is what makes the + protocol *robust* rather than relying on optimistic liveness. +3. *Deterministic exclusion.* The next attempt's participant set is a pure + function of the previous attempt's transition evidence (plus the + original group + seed). Two honest coordinators driving the same session + must arrive at the same attempt context. + +The byte-identical `EvaluateRetryParticipantsForSigning` shuffle satisfies +none of these. It re-shuffles the same set deterministically using +`(seed, retryCount)`. It has no notion of which participants were +*blamable* in the previous attempt, no exclusion ledger, and no message +context binding. + +The receive-loop drop is more subtle but equally protocol-violating: a +silent drop on channel overflow means that two participants observing the +same network can end up with divergent transcripts -- one with the +contribution, one without -- and there is no evidence trail to detect or +recover from that divergence. The `select { default }` pattern is fine for +optimistic transport but not as the canonical mechanism for protocol +membership. + +The two findings cluster naturally: + +* M4 (the receive drop) is the source of evidence. +* M7 (the retry shuffle) is the consumer of evidence. + +A change that fixes M7 without M4 has nothing to drive retry decisions on. +A change that fixes M4 without M7 produces evidence that no consumer reads. +This RFC therefore treats them as one design split into phases, not as two +independent fixes. + +== Background: ROAST in brief + +For implementers approaching this RFC fresh, the relevant ROAST surface is: + +* A *session* fixes the key group, the message digest, and the original + signer set. +* Each session goes through one or more *attempts*. An attempt is + identified by `(session_id, attempt_number)` and contains an *included + set* of participants and an *excluded set* of participants known to be + unable or unwilling to complete this attempt. +* The *coordinator* of an attempt is selected deterministically from the + included set (this is already implemented in `pkg/frost/roast/coordinator.go` + via `SelectCoordinator`). +* The coordinator collects round-one commitments, round-two signature + shares, then either: +** Aggregates a signature when t-of-n shares arrive within the attempt's + time bound -- the session is done. +** Times out and emits *transition evidence*: the set of peers that did + not contribute on time, and the new excluded set the next attempt + should use. + +The retry shuffle in keep-core's tECDSA path predates ROAST and answers a +different question -- "if signing fails, who do we try next?". It does so +without distinguishing inactive peers from corrupted ones, and it makes no +attempt to construct an evidence trail. That is appropriate for tECDSA +(which has its own malicious-share detection downstream) and inappropriate +for FROST (which expects the coordinator to be the source of truth for +attempt advancement). + +== Current state + +=== Retry layer + +`pkg/frost/retry/retry.go` exports `EvaluateRetryParticipantsForSigning` +and `EvaluateRetryParticipantsForKeyGeneration`. Both are pure shuffles +seeded by `(seed, retryCount)`. The signing variant takes no input from +the previous attempt's transcript. `diff pkg/frost/retry/retry.go +pkg/tecdsa/retry/retry.go` is empty. + +=== Coordinator layer + +`pkg/frost/roast/coordinator.go` exports `SelectCoordinator`. The function +is correct in isolation -- given an included set and an attempt context it +returns a deterministic coordinator -- but there is no consumer of the +selected coordinator's state. Attempt context is reconstructed +implicitly from `(seed, retryCount)` at the retry layer, with no shared +record of which messages arrived in which attempt. + +=== Receive layer + +`pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go` and +`pkg/frost/signing/native_frost_protocol_frost_native.go` together host +three receive loops: + +* `native_ffi_primitive_transitional_frost_native.go:973` -- tbtc-signer + round contribution capture. +* `native_frost_protocol_frost_native.go:568` -- native FROST round-one + commitments. +* `native_frost_protocol_frost_native.go:650` -- native FROST round-two + signature shares. + +All three use the same shape: + +[source,go] +---- +messageChan := make(chan *T, expectedMessagesCount*4+1) +request.Channel.Recv(recvCtx, func(message net.Message) { + // shouldAcceptNativeFROSTMessage(...) filtering ... + select { + case messageChan <- payload: + default: + } +}) +---- + +The channel is generously sized (`expected*4+1`), and the assembly side +applies first-write-wins / equal-or-reject (added in PR #3959). But the +`default` arm is still a *silent drop*. When it triggers, the protocol +has no trail to point to: no log of the dropped sender, no count of +drops by sender, no signal to the coordinator that this peer is being +under-represented. + +== Proposed design + +The design is three layers tied together by a single shared *attempt +context* type. + +=== Shared types + +A new package `pkg/frost/roast/attempt` introduces: + +[source,go] +---- +type AttemptContext struct { + SessionID string + KeyGroupID string + MessageDigest [32]byte + AttemptNumber uint + IncludedSet []group.MemberIndex + ExcludedSet []group.MemberIndex + AttemptSeed [32]byte +} + +func (AttemptContext) Hash() [32]byte +---- + +`AttemptContext.Hash()` becomes the canonical binding for every protocol +message emitted in the attempt -- contribution messages already carry a +`SessionIDValue`; we extend them with an `AttemptContextHash` field so +the receiver can reject stale-attempt messages structurally instead of +relying on session ID alone. + +`AttemptSeed` is widened from `int64` to `[32]byte` and *must* be +derived from inputs the group already agrees on -- specifically: + +[source,go] +---- +AttemptSeed = SHA256( + DkgGroupPublicKey || SessionID || MessageDigest, +) +---- + +This binding prevents a malicious coordinator from picking a seed that +shapes the included set in its favour. The seed is a pure function of +session inputs; it is never chosen, only derived. Any signer can +recompute it from the session header and verify the coordinator's +participant selection. + +*`DkgGroupPublicKey` source.* The runtime extracts `DkgGroupPublicKey` +from the FFI signer material at attempt construction time -- the same +material that already carries the DKG-validated group public key and is +required at signature-verification time anyway. Do not re-read it from +the wallet registry: the FFI material is the canonical hot-path source, +removes async/DB lookup latency, and preserves separation between the +core signing protocol and application state. + +=== Layer A: Receiver transition evidence (M4) + +The three `select { default }` drops become: + +[source,go] +---- +select { +case messageChan <- payload: +default: + evidence.RecordOverflow(payload.SenderID(), attemptCtx) +} +---- + +`evidence` is an `AttemptEvidenceRecorder` instantiated per attempt by +the coordinator-aware caller. It tracks: + +* Overflow events keyed by sender -- a sender that overflows + repeatedly is suspect either of attack or of being on a degraded + link, and the next attempt should treat the channel as evidence + rather than dropping it. +* Reject events keyed by sender and reason + (`shouldAcceptNativeFROSTMessage` returning false already). +* First-write-wins conflicts keyed by sender -- already logged in + PR #3959 but not yet structured into evidence. +* Per-attempt time bound expiry -- which senders failed to respond at + all before the attempt's context deadline. + +The recorder produces a `TransitionEvidence` value when the attempt +completes (either by signature aggregation or by timeout), which the +coordinator consumes. The recorder itself never decides who is excluded; +it only collects. + +Bounded means bounded: the recorder has a fixed-size ring *per sender, +per blame category*. The categories are tracked with separate quotas so +one category cannot mask another -- a peer cannot spam overflow events +to drown out reject evidence or vice-versa: + +[source,go] +---- +type categoryQuota struct { + Overflow uint8 // default 8 + Reject uint8 // default 8 + Conflict uint8 // default 4 + Silence uint8 // default 1 (single bit per attempt deadline) +} +---- + +The point is to produce a fixed-size attestation, not to log +everything forever. Per-attempt evidence is at most +`O(|IncludedSet| * sum(quotas))` bytes -- bounded, predictable, and +small enough to be signed and broadcast as a single message. The +broadcast mechanism is the coordinator-aggregated `TransitionMessage` +defined in the Resolved decisions section. + +=== Layer B: Coordinator state (joining M4 and M7) + +`pkg/frost/roast/coordinator.go` grows from a single selection function +into a state machine: + +[source,go] +---- +type AttemptState int +const ( + AttemptPending AttemptState = iota + AttemptCollecting + AttemptAggregating + AttemptSucceeded + AttemptTransitioned +) + +type Coordinator interface { + BeginAttempt(ctx AttemptContext) (AttemptHandle, error) + RecordEvidence(handle AttemptHandle, evidence TransitionEvidence) error + NextAttempt(handle AttemptHandle) (AttemptContext, error) +} +---- + +`NextAttempt` is the policy function that produces the next attempt's +context from the previous attempt's evidence. It is deterministic given +`(AttemptContext, TransitionEvidence)` -- two coordinators with the same +verified inputs agree on the next attempt without further coordination. + +The verified-inputs requirement is critical: gossip is eventually +consistent, but `NextAttempt` is a synchronous state transition. Two +honest signers fed differently-timed evidence sets produce divergent +contexts. To prevent that, the *evidence input itself* is an +authoritative `TransitionMessage` produced by the current attempt's +coordinator (the "coordinator-aggregation" model defined in the +Resolved decisions section); see that section for the full +agreement-flow specification. + +*Seed-bridging.* The legacy `pkg/frost/roast/coordinator.go::SelectCoordinator` +helper accepts an `int64` seed plus an attempt number. `BeginAttempt` +wraps it with a sterile bridge that folds the new `[32]byte` +`AttemptSeed` into the legacy parameter shape -- for example, taking +the first 8 bytes as a big-endian `int64`. The bridge is a +non-cryptographic adapter for the deterministic shuffle: equivalent +seed bytes must produce the same legacy `int64` on every honest +signer. The bridge is named, isolated, and exhaustively tested so +later edits cannot accidentally desynchronise it. + +The exclusion policy is: + +. Senders with `OverflowCount >= overflowExclusionThreshold` during the + attempt window are moved to `ExcludedSet` (transport blamable). +. Senders with at least one confirmed reject event for non-transport + reasons are moved to `ExcludedSet` (validation blamable). +. Senders with deadline-expiry only -- silent peers -- are moved to a + *parked* set that the next attempt skips but the attempt after that + retries (to tolerate transient outages). Silence parking is + *strictly transient*: a single attempt's worth of skip, no escalation. + A peer falsely labelled silent because their contribution arrived + late (or because a malicious coordinator censored it) is not + permanently penalised -- they are reinstated by the very next + attempt. Permanent exclusion only follows from overflow or non- + transport reject events, neither of which can fire on a slow-but- + honest peer. +. If `IncludedSet` minus exclusions drops below the threshold `t`, the + coordinator returns `ErrAttemptInfeasible` and the session is + declared failed for this signer set. + +The thresholds are *fixed constants* in the initial design, picked to +be evidently small relative to the per-attempt deadline and the +`expectedMessagesCount*4+1` channel capacity: + +[source,go] +---- +const ( + overflowExclusionThreshold = 4 // overflow events per attempt window + rejectExclusionThreshold = 1 // any confirmed non-transport reject + silenceParkingThreshold = 1 // any deadline expiry parks for 1 attempt +) +---- + +Making them constants up-front means honest signers do not need to +negotiate them. If production telemetry indicates a constant is wrong +for the attempt's wall-clock bound, the change is a routine code +update that ships through Phase 7's manifest gate -- not a runtime +parameter that drift can desynchronise. + +=== Layer C: Retry orchestration (M7) + +`pkg/frost/retry/retry.go` is renamed to +`pkg/frost/retry/retry_legacy.go` and kept for the key-generation path +(which already has its own three-tier exclusion structure that is closer +to ROAST semantics). The signing path moves to a thin wrapper around +`Coordinator.NextAttempt`: + +[source,go] +---- +func EvaluateRoastRetryForSigning( + coordinator Coordinator, + handle AttemptHandle, +) ([]group.MemberIndex, AttemptContext, error) +---- + +The byte-identical-to-tECDSA `EvaluateRetryParticipantsForSigning` is +removed once all callers migrate. We keep a `roast.SigningRetryAdapter` +shim implementing the old signature that delegates to the coordinator, +to make the migration mechanical PR-by-PR. + +== Phased implementation + +Each phase is one or two PRs. Phases are linear: later PRs assume +earlier PRs have merged. + +=== Phase 0: This RFC + +Doc-only. Lands first so subsequent code PRs can reference its design +choices in their PR descriptions and reviews. + +=== Phase 1: Attempt context type and hash + +* Add `pkg/frost/roast/attempt` package with `AttemptContext` and + canonical hash. No protocol behaviour changes. +* Extend protocol message structs with `AttemptContextHash` field, with + the field optional during the migration so existing peers stay + compatible. + +=== Phase 2: Receiver overflow tracking (M4 layer A) + +* Introduce `AttemptEvidenceRecorder` interface and a no-op default. +* Plumb the recorder through the three receive loops. Default no-op + preserves exact current behaviour. +* Add unit tests showing the recorder captures overflow without + changing receive semantics in the noop path. + +=== Phase 3: Coordinator state machine + +* Promote `pkg/frost/roast/coordinator.go` to a state-tracking + coordinator. Existing `SelectCoordinator` becomes an internal helper. +* Cover deterministic next-attempt computation under unit tests with + property tests for the + `(AttemptContext, TransitionEvidence) -> AttemptContext` map. +* No production code path uses the new coordinator yet -- it ships + unused. + +=== Phase 4: Wire receiver to coordinator + +* Connect the evidence recorder to a real coordinator instance behind + a new build tag (`frost_roast_retry`). +* Existing receive loops still use the noop recorder; the new code + path is reachable only when the build tag is set. +* Add a soak-style test that drives the full attempt -> evidence -> + next-attempt loop under fault injection (synthetic overflow, + synthetic reject, synthetic silence). + +=== Phase 5: Retry adapter, session orchestration, readiness gate + +* Add `EvaluateRoastRetryForSigning` and `roast.SigningRetryAdapter` + with a `ChainAddressResolver` interface to bridge + `group.MemberIndex` (ROAST namespace) to `chain.Address` (legacy + namespace). +* Wire session orchestration at the layer that constructs + `NativeExecutionFFISigningRequest`: at session start call + `Coordinator.BeginAttempt` and `SetCurrentAttemptHandleForSession`; + on session end call `ClearCurrentAttemptHandleForSession` from a + deferred cleanup so success and failure paths converge. +* Add a TTL eviction sweep over `sessionAttemptBindings` (default + two hours) so a goroutine panic before the deferred clear cannot + leak bindings indefinitely (see Resolved decisions). +* Wire a feature-flagged readiness gate + (`KEEP_CORE_FROST_ROAST_RETRY_ENABLED=true`) so production builds + with the `frost_roast_retry` tag still refuse to wire orchestration + without explicit operator opt-in. The gate matches the precedent + set by `KEEP_CORE_FROST_TBTC_SIGNER_ACCEPT_SCAFFOLD_KEY_GROUP`. + +*Important:* Phase 5 ships *no* signing-call-site migrations. The +adapter exists and is fully wired, but no production receive loop +calls it yet. A partial migration (round-one on ROAST, round-two on +legacy shuffle within the same session) would fracture the +attempt-context binding across the two rounds and disconnect the +evidence captured in round-one from the participant selection in +round-two. The readiness gate -- not partial migration -- is the +risk-management mechanism. + +=== Phase 6: Migrate all signing call sites + +* Migrate *all three* signing call sites onto the adapter in a single + coordinated change: +** `collectNativeFROSTRoundOneMessages` +** `collectNativeFROSTRoundTwoMessages` +** `collectBuildTaggedTBTCSignerRoundContributionMessages` ++ +The three flows share one attempt context per session; migrating +them together preserves the round-to-round evidence binding within a +session. +* Once the legacy `EvaluateRetryParticipantsForSigning` has no + callers, delete it. (Key-generation legacy retry stays.) +* Remove the `frost_roast_retry` build tag; the new retry path is + unconditional once Phase 7's manifest gate flips. + +=== Phase 7: Readiness manifest evidence + +* Update the FROST readiness manifest to flip ROAST retry + + transition evidence from `missing-no-go` to `present` once Phase 6 + ships and the integration test suite has been run against a real + testnet. +* As with every readiness gate in this repo, the manifest is updated + only when the supporting evidence is attached. The RFC does not + promise an early flip. + +== Resolved decisions + +The decisions in this section were settled in a Phase-3 design review +(2026-05-22) with cross-team protocol-owner input. They are listed +here so subsequent implementation PRs can reference them. + +=== Cross-process coordinator agreement + +*Decision: coordinator-proposed aggregation on a dedicated topic, +signed with the operator key, with receiver-side bundle verification +for censorship detection.* + +The earlier draft of this RFC carried "all-to-all signed-evidence +gossip with local union" as the recommended path. That recommendation +silently assumed gossip is synchronously consistent across the signer +set; in practice gossip is eventually consistent, so two honest +signers can hold divergent evidence sets at the moment the attempt +times out. Applying the deterministic `NextAttempt` function to +divergent inputs produces divergent next-attempt contexts and +fractures the signing group. + +The replacement flow is: + +. *Observation.* Each signer's `EvidenceRecorder` (Phase 2) + produces a per-attempt local-evidence snapshot. +. *Submission.* Each signer signs its snapshot with its operator + key (the same key `pkg/net` already uses to attribute network + messages) and broadcasts it on a dedicated evidence topic. +. *Aggregation.* The current attempt's elected coordinator + (the deterministic `SelectCoordinator` output) collects the + signed snapshots, builds a canonical bundle, signs the bundle, + and broadcasts it as a `TransitionMessage`. +. *Verification.* Every receiver validates the bundle's + coordinator signature, validates each contained snapshot's + operator signature, *and verifies that its own observations + appear in the bundle*. A coordinator that omits an honest + peer's signed snapshot is caught here. +. *Transition.* Receivers feed the verified bundle into + `NextAttempt`. Because the bundle is the authoritative input, + all honest receivers compute the same next-attempt context. + +A peer that signs conflicting snapshots is slashable -- the +signature is the binding. A coordinator that signs an inconsistent +bundle (omits observations, alters counts, etc.) is detected at +verification step (4) and the next-attempt coordinator handles the +exclusion. + +Alternatives considered (rejected): + +. *All-to-all signed-evidence gossip with local union.* Original + recommendation. Rejected because gossip's eventual-consistency + semantics let honest signers reach the deterministic + `NextAttempt` boundary with divergent inputs, producing + divergent outputs. +. *Piggy-back on existing FROST broadcast channel.* Rejected + because it couples evidence rate limits to protocol round-trip + rate limits, and re-uses a topic with different traffic + characteristics. +. *Coordinator-only authoritative without aggregation.* Rejected + because losing the all-signer signed attestations also loses + the audit trail. The aggregation model keeps the per-signer + signatures inside the bundle, so the audit trail survives. + +Liveness: a malicious coordinator can withhold the +`TransitionMessage`, stalling the transition. ROAST handles this +the same way it handles a malicious signer: the attempt times +out, the next attempt elects a different coordinator (the +`SelectCoordinator` output is deterministic but rotates with the +attempt number), and the new coordinator drives the transition. +The malicious coordinator's evidence is itself parked or +excluded by the new coordinator's bundle, ending the loop. + +Safety: any honest signer that verifies a bundle and computes +`NextAttempt(ctx, bundle)` produces the same context as any other +honest signer that verifies the same bundle. Safety reduces to +"is the bundle correctly verified" -- a local check, not a +network-consistency requirement. + +This design satisfies the formal verified-inputs requirement of +the deterministic `NextAttempt` policy specified in Layer B. + +=== Source of `DkgGroupPublicKey` for the seed + +*Decision: extract from FFI signer material at attempt construction.* + +The DKG-validated group public key is already present in the FFI +signer material (it is required at signature-verification time +anyway), so the seed derivation can take it from there. The +wallet registry is *not* consulted on the hot path; doing so +would introduce async lookup latency and entangle the core +signing protocol with application state. See Shared types above +for the derivation contract. + +=== `AttemptContext` ↔ `NativeExecutionFFISigningRequest` binding + +*Decision: extend the request struct with an `AttemptContext` +field; the context is Go-side orchestration only.* + +The context does not cross the CGO/Rust boundary into the +`tbtc-signer` engine -- the engine remains a pure signing +primitive. Go-side coordinator wiring populates the context; +existing call sites construct attempt-zero contexts inline +during Phase 4. + +=== `SelectCoordinator` retention + +*Decision: keep the existing helper; bridge the seed type inside +`BeginAttempt`.* + +The deterministic shuffle is correct in isolation. The bridge +folds the new `[32]byte` `AttemptSeed` into the legacy `int64` +parameter shape with a sterile, named adapter (see Layer B). + +=== Evidence-signing key + +*Decision: reuse the existing operator key.* + +The operator key already binds every other gossip message a +keep-core node emits via `pkg/net`. Layering a second key +surface specifically for evidence signing is premature +optimization given the current key model. + +=== Evidence message format + +*Decision: JSON payload wrapped in the existing `pkg/net/gen/pb` +envelope, routed via the `net.Message` interface.* + +This matches the FROST/tbtc-signer protocol messages (Phase 1B) +and inherits the network layer's operator-key signing +automatically. Raw JSON does not appear on the wire. + +=== Maximum evidence-message size + +*Decision: single `TransitionMessage` per transition; no +chunking.* + +Under coordinator-aggregation, the per-transition payload is +`O(N)` not `O(N^2)`. At a 100-signer group with all four +quotas saturated the JSON-encoded bundle is ~10-20 KiB, +comfortably within libp2p's per-message limits. + +=== Session-handle binding TTL eviction + +*Decision: a periodic sweep over `sessionAttemptBindings` +(introduced by Phase 4.3) evicts entries older than a fixed TTL +(default two hours).* + +Phase 4.3's session-handle registry expects the orchestration +layer (Phase 5) to call `ClearCurrentAttemptHandleForSession` +from a deferred cleanup when the session ends. A goroutine +panic before the deferred clear runs would leak the binding +permanently, since nothing else removes it. + +The two-hour TTL is a defence-in-depth backstop. It is long +enough that no real signing session reaches it (typical sessions +complete in seconds; a multi-attempt session under ROAST retry +should not exceed minutes), and short enough that a leaked +binding does not accumulate across days of node uptime. The +sweep itself runs on a background goroutine; entries are evicted +in batches under the registry's existing write lock. + +Phase 5.2 introduces both the sweep goroutine and the timestamp +field on `sessionAttemptBinding`. The eviction does not depend +on session-completion correctness; it only catches the +panic-before-defer pathological case. + +=== Orchestration error taxonomy + +*Decision: orchestration errors are bucketed into two classes, with +fundamentally different handling.* + +When the executor adapter calls `BeginOrchestrationForSession`, +the call can fail for two distinct reasons that the receiver MUST +distinguish: + +. *Static-configuration errors* -- the build was deployed without + the readiness env var, or no caller has registered a coordinator. + These errors are fully deterministic: every honest signer + observes the same error from the same configuration at the same + startup state. Safe to log at INFO and fall back to the legacy + retry path. Sentinel errors `ErrRoastRetryReadinessOptOut` and + `ErrNoRoastRetryCoordinatorRegistered` (introduced in Phase 6.3) + signal this class; `errors.Is` checks identify them. + +. *Runtime state-machine errors* -- `Coordinator.BeginAttempt` + returned an error (out-of-memory, malformed AttemptContext, + internal invariant violated, etc.). These errors are + non-deterministic across nodes: node A may experience a runtime + failure while node B succeeds. Treat them as hard failures: + return an error from the executor adapter, declare the session + failed. + +The safety reason is load-bearing. If node A falls back to the +legacy retry shuffle while node B proceeds with the ROAST state +machine, the two nodes compute different `NextAttempt` participant +sets, and the signing group fractures permanently. The legacy +fallback is only acceptable when every honest signer would make +the same choice, which is true for static configuration and false +for runtime errors. + +This decision applies to Phase 6.3 (orchestration wiring at the +executor adapter) and Phase 6.4 (call-site migration). Phase 5 +deliberately ships orchestration as best-effort because it has no +production consumer; Phase 6 is where the safety distinction +matters. + +=== `FrostUniFFIV1` signer-material prerequisite + +*Decision: Phase 7's manifest flip is gated on verified migration +away from `FrostUniFFIV1` signer material across all production +signers.* + +Phase 6.1's `ExtractDkgGroupPublicKeyFromMaterial` switches on +`NativeSignerMaterial.Format`. The two production-relevant formats +(`FrostUniFFIV2` and `FrostTBTCSignerV1`) expose the DKG group +public key on the material directly. The legacy `FrostUniFFIV1` +format does not include the group key in a form Phase 6.1 can +extract; the helper returns a descriptive error directing +operators to migrate. + +Until the network has fully migrated off V1, the Phase 7 readiness +manifest cannot flip to `present`. The migration tracking +mechanism is out of scope for this RFC; the prerequisite is +documented here as a hard dependency of Phase 7. + +== Open questions + +. *Persistence across signer restart.* If a signer crashes mid-attempt, + does it lose its evidence? The paper assumes persistent state. For + keep-core we likely accept evidence loss on restart at first (the + attempt times out and a new attempt is started fresh) and revisit + persistence in a follow-up RFC once we have wire-level evidence. ++ +*Sketch for Phase 5+:* introduce a `SyncState` gossip message. A +restarting node broadcasts +`(LastKnownAttemptContextHash, KeyGroupID)`; peers reply with their +current attempt and the set of signed attestations they hold for +that attempt. This avoids the timeout-and-restart cost on graceful +redeploys without requiring on-disk persistence -- the peers' +gossiped attestations *are* the persistent record. +. *FFI surface.* `tbtc-signer` (the Rust engine) does not need to know + about ROAST coordinator state -- it remains a pure signing engine. + But it does need to surface structured errors that the coordinator + can map to exclusion reasons. PR #425 / #3961 (the L5 paired + change) is the template for this style of error-code wiring. Future + exclusion-relevant errors should follow the same dedicated-variant + pattern. +. *Backward-compat horizon.* Once the `AttemptContextHash` field is on + protocol messages, how long do we accept messages from peers that + omit it? Proposal: optional during Phase 1-5, required at Phase 6, + validation-rejection at Phase 7. + +== Out of scope + +* DKG retry. The key-generation legacy retry stays. Re-evaluating DKG + retry under ROAST is a separate RFC. +* Bitcoin transaction-builder changes. Witness restoration and + P2WSH/P2TR handling are unaffected. +* Operator UX (CLI flags, dashboards). Whatever is needed lands + alongside Phase 5 / Phase 6 as small, focused PRs. +* Cross-domain ROAST (e.g., between keep-core and tbtc-signer). The + signer remains a single-process engine; coordinator state lives on + the keep-core side. + +== References + +* Ruffing, Ronge, Aranha, Schneider. ``ROAST: Robust Asynchronous + Schnorr Threshold Signatures.'' ACM CCS 2022. +* Komlo, Goldberg. ``FROST: Flexible Round-Optimized Schnorr Threshold + Signatures.'' SAC 2020. +* RFC-20: Schnorr/FROST Migration Scaffold (`docs/rfc/rfc-20-schnorr-frost-migration-scaffold.adoc`). +* Independent review of FROST/ROAST readiness branch: + https://github.com/threshold-network/keep-core/pull/3866. +* L5 paired error-code change: `tlabs-xyz/tbtc#425` (Rust producer) + + `threshold-network/keep-core#3961` (Go consumer). +* Receive-loop drop sites: +** `pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go:973` +** `pkg/frost/signing/native_frost_protocol_frost_native.go:568` +** `pkg/frost/signing/native_frost_protocol_frost_native.go:650` +* Byte-identical retry shuffle: +** `pkg/frost/retry/retry.go` +** `pkg/tecdsa/retry/retry.go` diff --git a/go.mod b/go.mod index 33af079464..6e1a8d41c6 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pion/datachannel v1.5.10 // indirect diff --git a/pkg/bitcoin/electrum/electrum.go b/pkg/bitcoin/electrum/electrum.go index 70cbc82309..f94fe471ef 100644 --- a/pkg/bitcoin/electrum/electrum.go +++ b/pkg/bitcoin/electrum/electrum.go @@ -434,6 +434,56 @@ func (c *Connection) GetTransactionsForPublicKeyHash( return transactions, nil } +// GetTransactionsForPublicKeyScripts gets confirmed transactions that pay to +// any of the given public key scripts. The returned transactions are ordered +// by block height in ascending order, i.e. the latest transaction is at the +// end of the list. +func (c *Connection) GetTransactionsForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, + limit int, +) ([]*bitcoin.Transaction, error) { + txHashes, err := c.GetTxHashesForPublicKeyScripts(publicKeyScripts) + if err != nil { + return nil, err + } + + selectedTxHashes := selectLatestUniqueTxHashes(txHashes, limit) + + transactions := make([]*bitcoin.Transaction, len(selectedTxHashes)) + for i, txHash := range selectedTxHashes { + transaction, err := c.GetTransaction(txHash) + if err != nil { + return nil, fmt.Errorf("cannot get transaction: [%v]", err) + } + + transactions[i] = transaction + } + + return transactions, nil +} + +func selectLatestUniqueTxHashes( + txHashes []bitcoin.Hash, + limit int, +) []bitcoin.Hash { + uniqueTxHashes := make([]bitcoin.Hash, 0, len(txHashes)) + seen := make(map[bitcoin.Hash]bool) + for _, txHash := range txHashes { + if seen[txHash] { + continue + } + + seen[txHash] = true + uniqueTxHashes = append(uniqueTxHashes, txHash) + } + + if len(uniqueTxHashes) > limit { + return uniqueTxHashes[len(uniqueTxHashes)-limit:] + } + + return uniqueTxHashes +} + // GetTxHashesForPublicKeyHash gets hashes of confirmed transactions that pays // the given public key hash using either a P2PKH or P2WPKH script. The returned // transactions hashes are ordered by block height in the ascending order, i.e. @@ -461,26 +511,19 @@ func (c *Connection) GetTxHashesForPublicKeyHash( ) } - p2pkhItems, err := c.getConfirmedScriptHistory(p2pkh) - if err != nil { - return nil, fmt.Errorf( - "cannot get P2PKH history for public key hash [0x%x]: [%v]", - publicKeyHash, - err, - ) - } + return c.GetTxHashesForPublicKeyScripts([]bitcoin.Script{p2pkh, p2wpkh}) +} - p2wpkhItems, err := c.getConfirmedScriptHistory(p2wpkh) +// GetTxHashesForPublicKeyScripts gets hashes of confirmed transactions that +// pay to any of the given public key scripts. +func (c *Connection) GetTxHashesForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, +) ([]bitcoin.Hash, error) { + items, err := c.getConfirmedScriptHistories(publicKeyScripts) if err != nil { - return nil, fmt.Errorf( - "cannot get P2WPKH history for public key hash [0x%x]: [%v]", - publicKeyHash, - err, - ) + return nil, err } - items := append(p2pkhItems, p2wpkhItems...) - sort.SliceStable( items, func(i, j int) bool { @@ -496,6 +539,27 @@ func (c *Connection) GetTxHashesForPublicKeyHash( return txHashes, nil } +func (c *Connection) getConfirmedScriptHistories( + publicKeyScripts []bitcoin.Script, +) ([]*scriptHistoryItem, error) { + items := make([]*scriptHistoryItem, 0) + + for _, publicKeyScript := range publicKeyScripts { + scriptItems, err := c.getConfirmedScriptHistory(publicKeyScript) + if err != nil { + return nil, fmt.Errorf( + "cannot get history for script [0x%x]: [%v]", + publicKeyScript, + err, + ) + } + + items = append(items, scriptItems...) + } + + return items, nil +} + type scriptHistoryItem struct { txHash bitcoin.Hash blockHeight int32 @@ -752,45 +816,15 @@ func (c *Connection) GetUtxosForPublicKeyHash( ) } - p2pkhItems, err := c.getScriptUtxos(p2pkh, true) - if err != nil { - return nil, fmt.Errorf( - "cannot get P2PKH UTXOs for public key hash [0x%x]: [%v]", - publicKeyHash, - err, - ) - } - - p2wpkhItems, err := c.getScriptUtxos(p2wpkh, true) - if err != nil { - return nil, fmt.Errorf( - "cannot get P2WPKH UTXOs for public key hash [0x%x]: [%v]", - publicKeyHash, - err, - ) - } - - items := append(p2pkhItems, p2wpkhItems...) - - sort.SliceStable( - items, - func(i, j int) bool { - return items[i].blockHeight < items[j].blockHeight - }, - ) - - utxos := make([]*bitcoin.UnspentTransactionOutput, len(items)) - for i, item := range items { - utxos[i] = &bitcoin.UnspentTransactionOutput{ - Outpoint: &bitcoin.TransactionOutpoint{ - TransactionHash: item.txHash, - OutputIndex: item.outputIndex, - }, - Value: int64(item.value), - } - } + return c.GetUtxosForPublicKeyScripts([]bitcoin.Script{p2pkh, p2wpkh}) +} - return utxos, nil +// GetUtxosForPublicKeyScripts gets unspent outputs of confirmed transactions +// that are controlled by any of the given public key scripts. +func (c *Connection) GetUtxosForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, +) ([]*bitcoin.UnspentTransactionOutput, error) { + return c.getUtxosForPublicKeyScripts(publicKeyScripts, true) } // GetMempoolUtxosForPublicKeyHash gets unspent outputs of unconfirmed transactions @@ -820,26 +854,35 @@ func (c *Connection) GetMempoolUtxosForPublicKeyHash( ) } - p2pkhItems, err := c.getScriptUtxos(p2pkh, false) + return c.GetMempoolUtxosForPublicKeyScripts([]bitcoin.Script{p2pkh, p2wpkh}) +} + +// GetMempoolUtxosForPublicKeyScripts gets unspent outputs of unconfirmed +// transactions that are controlled by any of the given public key scripts. +func (c *Connection) GetMempoolUtxosForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, +) ([]*bitcoin.UnspentTransactionOutput, error) { + return c.getUtxosForPublicKeyScripts(publicKeyScripts, false) +} + +func (c *Connection) getUtxosForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, + confirmed bool, +) ([]*bitcoin.UnspentTransactionOutput, error) { + items, err := c.getScriptUtxosForScripts(publicKeyScripts, confirmed) if err != nil { - return nil, fmt.Errorf( - "cannot get P2PKH UTXOs for public key hash [0x%x]: [%v]", - publicKeyHash, - err, - ) + return nil, err } - p2wpkhItems, err := c.getScriptUtxos(p2wpkh, false) - if err != nil { - return nil, fmt.Errorf( - "cannot get P2WPKH UTXOs for public key hash [0x%x]: [%v]", - publicKeyHash, - err, + if confirmed { + sort.SliceStable( + items, + func(i, j int) bool { + return items[i].blockHeight < items[j].blockHeight + }, ) } - items := append(p2pkhItems, p2wpkhItems...) - utxos := make([]*bitcoin.UnspentTransactionOutput, len(items)) for i, item := range items { utxos[i] = &bitcoin.UnspentTransactionOutput{ @@ -854,6 +897,28 @@ func (c *Connection) GetMempoolUtxosForPublicKeyHash( return utxos, nil } +func (c *Connection) getScriptUtxosForScripts( + publicKeyScripts []bitcoin.Script, + confirmed bool, +) ([]*scriptUtxoItem, error) { + items := make([]*scriptUtxoItem, 0) + + for _, publicKeyScript := range publicKeyScripts { + scriptItems, err := c.getScriptUtxos(publicKeyScript, confirmed) + if err != nil { + return nil, fmt.Errorf( + "cannot get UTXOs for script [0x%x]: [%v]", + publicKeyScript, + err, + ) + } + + items = append(items, scriptItems...) + } + + return items, nil +} + type scriptUtxoItem struct { txHash bitcoin.Hash outputIndex uint32 diff --git a/pkg/bitcoin/script.go b/pkg/bitcoin/script.go index b6bdf144dc..405ea6ec08 100644 --- a/pkg/bitcoin/script.go +++ b/pkg/bitcoin/script.go @@ -19,6 +19,7 @@ const ( P2WPKHScript P2SHScript P2WSHScript + P2TRScript ) func (st ScriptType) String() string { @@ -31,6 +32,8 @@ func (st ScriptType) String() string { return "P2SH" case P2WSHScript: return "P2WSH" + case P2TRScript: + return "P2TR" default: return "NonStandard" } @@ -147,8 +150,25 @@ func PayToScriptHash(scriptHash [20]byte) (Script, error) { Script() } +// PayToTaproot constructs a P2TR script for the provided 32-byte x-only +// Taproot output key. The function assumes the provided output key is valid. +// +// The argument must be the final Taproot output key committed to by the +// scriptPubKey. This helper does not derive a BIP-341/BIP-86 tweak from an +// internal key. +func PayToTaproot(outputKey [32]byte) (Script, error) { + return txscript.NewScriptBuilder(). + AddOp(txscript.OP_1). + AddData(outputKey[:]). + Script() +} + // GetScriptType gets the ScriptType of the given Script. func GetScriptType(script Script) ScriptType { + if isPayToTaproot(script) { + return P2TRScript + } + switch txscript.GetScriptClass(script) { case txscript.PubKeyHashTy: return P2PKHScript @@ -163,6 +183,12 @@ func GetScriptType(script Script) ScriptType { } } +func isPayToTaproot(script Script) bool { + return len(script) == 34 && + script[0] == txscript.OP_1 && + script[1] == txscript.OP_DATA_32 +} + // ExtractPublicKeyHash extracts the public key hash from a P2WPKH or P2PKH // script. func ExtractPublicKeyHash(script Script) ([20]byte, error) { @@ -189,3 +215,15 @@ func ExtractPublicKeyHash(script Script) ([20]byte, error) { return publicKeyHash, nil } + +// ExtractTaprootKey extracts the x-only output key from a P2TR script. +func ExtractTaprootKey(script Script) ([32]byte, error) { + if GetScriptType(script) != P2TRScript { + return [32]byte{}, fmt.Errorf("not a P2TR script") + } + + var outputKey [32]byte + copy(outputKey[:], script[2:]) + + return outputKey, nil +} diff --git a/pkg/bitcoin/script_test.go b/pkg/bitcoin/script_test.go index 9de1d69869..6720b3b92b 100644 --- a/pkg/bitcoin/script_test.go +++ b/pkg/bitcoin/script_test.go @@ -9,6 +9,8 @@ import ( "testing" "github.com/btcsuite/btcd/btcec" + btcec2 "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/keep-network/keep-core/internal/testutils" ) @@ -334,6 +336,137 @@ func TestPayToScriptHash(t *testing.T) { testutils.AssertBytesEqual(t, expectedResult, result[:]) } +func TestPayToTaproot(t *testing.T) { + outputKeyBytes, err := hex.DecodeString( + "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + ) + if err != nil { + t.Fatal(err) + } + + var outputKey [32]byte + copy(outputKey[:], outputKeyBytes) + + result, err := PayToTaproot(outputKey) + if err != nil { + t.Fatal(err) + } + + expectedResult, err := hex.DecodeString( + "51201b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + ) + if err != nil { + t.Fatal(err) + } + + testutils.AssertBytesEqual(t, expectedResult, result[:]) +} + +func TestTaprootLeafHash(t *testing.T) { + script := Script(hexToSlice( + t, + "76a9140102030405060708090a0b0c0d0e0f101112131488ac", + )) + + result, err := TaprootLeafHash(script) + if err != nil { + t.Fatal(err) + } + + expectedResult := hexToSlice( + t, + "37a57b86de2819d2b72a173df46238a7ad295ea1485d3b40e9415daa82b4fdcb", + ) + + testutils.AssertBytesEqual(t, expectedResult, result[:]) +} + +func TestTaprootTweakAndOutputKey(t *testing.T) { + privateKey, _ := btcec2.PrivKeyFromBytes( + hexToSlice( + t, + "0101010101010101010101010101010101010101010101010101010101010101", + ), + ) + + var internalKey [32]byte + copy(internalKey[:], schnorr.SerializePubKey(privateKey.PubKey())) + + refundLeaf := Script(hexToSlice( + t, + "76a9140102030405060708090a0b0c0d0e0f101112131488ac", + )) + + merkleRoot, err := TaprootLeafHash(refundLeaf) + if err != nil { + t.Fatal(err) + } + + tweak, err := TaprootTweak(internalKey, &merkleRoot) + if err != nil { + t.Fatal(err) + } + + expectedTweak := hexToSlice( + t, + "6ca66b4600554f36d490d227669ba78c2d4778a8ecc07565ae2f9e87c28f124a", + ) + + testutils.AssertBytesEqual(t, expectedTweak, tweak[:]) + + outputKey, err := TaprootOutputKey(internalKey, &merkleRoot) + if err != nil { + t.Fatal(err) + } + + expectedOutputKey := hexToSlice( + t, + "b31d6b4f10bcea1dfcace63ce7defda9e718a4340b4b5befef6194488780ef17", + ) + + testutils.AssertBytesEqual(t, expectedOutputKey, outputKey[:]) +} + +func TestPayToTaprootWithScriptTree(t *testing.T) { + privateKey, _ := btcec2.PrivKeyFromBytes( + hexToSlice( + t, + "0202020202020202020202020202020202020202020202020202020202020202", + ), + ) + + var internalKey [32]byte + copy(internalKey[:], schnorr.SerializePubKey(privateKey.PubKey())) + + merkleRootBytes := hexToSlice( + t, + "b2c459126150e0d47063ea7b6d0474a24c39e25908aae5740dd4787b67c6e19a", + ) + var merkleRoot [32]byte + copy(merkleRoot[:], merkleRootBytes) + + result, err := PayToTaprootWithScriptTree(internalKey, merkleRoot) + if err != nil { + t.Fatal(err) + } + + expectedOutputKey := hexToSlice( + t, + "e339710a2348c113ade4a4e7d52bd1c12bc69818f1af7f41e161142701b93c96", + ) + + // Rebuild the expected P2TR script directly to avoid reusing + // PayToTaprootWithScriptTree. + var expectedKey [32]byte + copy(expectedKey[:], expectedOutputKey) + expectedResult, err := PayToTaproot(expectedKey) + if err != nil { + t.Fatal(err) + } + + testutils.AssertBytesEqual(t, expectedResult, result) +} + func TestGetScriptType(t *testing.T) { fromHex := func(hexString string) []byte { bytes, err := hex.DecodeString(hexString) @@ -363,6 +496,10 @@ func TestGetScriptType(t *testing.T) { script: fromHex("002086a303cdd2e2eab1d1679f1a813835dc5a1b65321077cdccaf08f98cbf04ca96"), expectedType: P2WSHScript, }, + "p2tr script": { + script: fromHex("51201b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"), + expectedType: P2TRScript, + }, "non-standard script": { script: fromHex( "14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508f9f0c90d0003" + @@ -387,6 +524,59 @@ func TestGetScriptType(t *testing.T) { } } +func TestExtractTaprootKey(t *testing.T) { + fromHex := func(hexString string) []byte { + bytes, err := hex.DecodeString(hexString) + if err != nil { + t.Fatal(err) + } + return bytes + } + + var outputKey [32]byte + copy( + outputKey[:], + fromHex("1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"), + ) + + var tests = map[string]struct { + script Script + expectedOutputKey [32]byte + expectedErr error + }{ + "P2TR script": { + script: fromHex("51201b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f"), + expectedOutputKey: outputKey, + }, + "other script": { + script: fromHex("00148db50eb52063ea9d98b3eac91489a90f738986f6"), + expectedErr: fmt.Errorf("not a P2TR script"), + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + actualOutputKey, err := ExtractTaprootKey(test.script) + + if !reflect.DeepEqual(test.expectedErr, err) { + t.Errorf( + "unexpected error\nexpected: %+v\nactual: %+v\n", + test.expectedErr, + err, + ) + } + + if test.expectedOutputKey != actualOutputKey { + t.Errorf( + "unexpected taproot output key\nexpected: 0x%x\nactual: 0x%x\n", + test.expectedOutputKey, + actualOutputKey, + ) + } + }) + } +} + func TestExtractPublicKeyHash(t *testing.T) { fromHex := func(hexString string) []byte { bytes, err := hex.DecodeString(hexString) diff --git a/pkg/bitcoin/taproot.go b/pkg/bitcoin/taproot.go new file mode 100644 index 0000000000..b3d44867f5 --- /dev/null +++ b/pkg/bitcoin/taproot.go @@ -0,0 +1,138 @@ +package bitcoin + +import ( + "bytes" + "fmt" + + btcec2 "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" +) + +const taprootBaseLeafVersion = 0xc0 + +// TaprootLeafHash computes the BIP-342 TapLeaf hash for a base-version script. +func TaprootLeafHash(script Script) ([32]byte, error) { + var buffer bytes.Buffer + + if err := buffer.WriteByte(taprootBaseLeafVersion); err != nil { + return [32]byte{}, err + } + + scriptLength, err := writeCompactSizeUint(CompactSizeUint(len(script))) + if err != nil { + return [32]byte{}, fmt.Errorf( + "cannot encode taproot script length: [%v]", + err, + ) + } + + if _, err := buffer.Write(scriptLength); err != nil { + return [32]byte{}, err + } + if _, err := buffer.Write(script); err != nil { + return [32]byte{}, err + } + + return taggedHashToArray( + chainhash.TaggedHash(chainhash.TagTapLeaf, buffer.Bytes()), + ), nil +} + +// TaprootTweak computes the BIP-341 TapTweak hash for an x-only internal key +// and optional script merkle root. +func TaprootTweak( + internalKey [32]byte, + merkleRoot *[32]byte, +) ([32]byte, error) { + _, _, tweak, err := taprootTweakScalar(internalKey, merkleRoot) + return tweak, err +} + +// TaprootOutputKey derives the BIP-341 tweaked x-only Taproot output key from +// an x-only internal key and optional script merkle root. +func TaprootOutputKey( + internalKey [32]byte, + merkleRoot *[32]byte, +) ([32]byte, error) { + internalPublicKey, tweakScalar, _, err := taprootTweakScalar( + internalKey, + merkleRoot, + ) + if err != nil { + return [32]byte{}, err + } + + var internalPoint btcec2.JacobianPoint + internalPublicKey.AsJacobian(&internalPoint) + + var tweakPoint btcec2.JacobianPoint + btcec2.ScalarBaseMultNonConst(&tweakScalar, &tweakPoint) + + var outputPoint btcec2.JacobianPoint + btcec2.AddNonConst(&internalPoint, &tweakPoint, &outputPoint) + + if outputPoint.Z.IsZero() { + return [32]byte{}, fmt.Errorf("taproot output key is infinity") + } + + outputPoint.ToAffine() + outputPublicKey := btcec2.NewPublicKey(&outputPoint.X, &outputPoint.Y) + + var outputKey [32]byte + copy(outputKey[:], schnorr.SerializePubKey(outputPublicKey)) + + return outputKey, nil +} + +// PayToTaprootWithScriptTree constructs a P2TR script from an internal key and +// a script merkle root by applying the BIP-341 TapTweak. +func PayToTaprootWithScriptTree( + internalKey [32]byte, + merkleRoot [32]byte, +) (Script, error) { + outputKey, err := TaprootOutputKey(internalKey, &merkleRoot) + if err != nil { + return nil, fmt.Errorf("cannot derive taproot output key: [%v]", err) + } + + return PayToTaproot(outputKey) +} + +func taprootTweakScalar( + internalKey [32]byte, + merkleRoot *[32]byte, +) (*btcec2.PublicKey, btcec2.ModNScalar, [32]byte, error) { + internalPublicKey, err := schnorr.ParsePubKey(internalKey[:]) + if err != nil { + return nil, btcec2.ModNScalar{}, [32]byte{}, fmt.Errorf( + "cannot parse taproot internal key: [%v]", + err, + ) + } + + tweakMessages := [][]byte{internalKey[:]} + if merkleRoot != nil { + tweakMessages = append(tweakMessages, merkleRoot[:]) + } + + tweak := taggedHashToArray( + chainhash.TaggedHash(chainhash.TagTapTweak, tweakMessages...), + ) + + var tweakScalar btcec2.ModNScalar + if overflow := tweakScalar.SetBytes(&tweak); overflow != 0 { + return nil, btcec2.ModNScalar{}, [32]byte{}, fmt.Errorf( + "taproot tweak is greater than or equal to curve order", + ) + } + + return internalPublicKey, tweakScalar, tweak, nil +} + +func taggedHashToArray(hash *chainhash.Hash) [32]byte { + var result [32]byte + copy(result[:], hash[:]) + + return result +} diff --git a/pkg/bitcoin/transaction_builder.go b/pkg/bitcoin/transaction_builder.go index 06e595d65d..3624ddc350 100644 --- a/pkg/bitcoin/transaction_builder.go +++ b/pkg/bitcoin/transaction_builder.go @@ -1,11 +1,16 @@ package bitcoin import ( + "bytes" "crypto/ecdsa" + "crypto/sha256" + "encoding/binary" + "encoding/hex" "fmt" "math/big" "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -32,8 +37,41 @@ func NewTransactionBuilder(chain Chain) *TransactionBuilder { } } +// HasTaprootKeyPathInputs returns true if the builder has at least one P2TR +// input intended to be spent using the Taproot key path. +func (tb *TransactionBuilder) HasTaprootKeyPathInputs() bool { + for _, sigHashArgs := range tb.sigHashArgs { + if sigHashArgs.scriptType == P2TRScript { + return true + } + } + + return false +} + +// HasOnlyTaprootKeyPathInputs returns true if every input in the builder is a +// P2TR input intended to be spent using the Taproot key path. +func (tb *TransactionBuilder) HasOnlyTaprootKeyPathInputs() bool { + if len(tb.sigHashArgs) == 0 { + return false + } + + for _, sigHashArgs := range tb.sigHashArgs { + if sigHashArgs.scriptType != P2TRScript { + return false + } + } + + return true +} + // AddPublicKeyHashInput adds an unsigned input pointing to a UTXO locked // using a P2PKH or P2WPKH script. +// +// For backward compatibility with wallet-action construction that discovers +// the input script type from the chain, this method also accepts P2TR direct +// key-path inputs. New Taproot-specific code should prefer +// AddTaprootKeyPathInput to make that spend policy explicit. func (tb *TransactionBuilder) AddPublicKeyHashInput( utxo *UnspentTransactionOutput, ) error { @@ -46,25 +84,133 @@ func (tb *TransactionBuilder) AddPublicKeyHashInput( ) } - class := txscript.GetScriptClass(utxoScript) - isPublicKeyHashScript := class == txscript.PubKeyHashTy || - class == txscript.WitnessV0PubKeyHashTy - if !isPublicKeyHashScript { + scriptType := GetScriptType(utxoScript) + isDirectKeySpendScript := scriptType == P2PKHScript || + scriptType == P2WPKHScript || + scriptType == P2TRScript + if !isDirectKeySpendScript { + return fmt.Errorf( + "UTXO pointed by the input is not P2PKH/P2WPKH/P2TR", + ) + } + + return tb.addDirectKeySpendInput(utxo, utxoScript, scriptType, nil) +} + +// AddTaprootKeyPathInput adds an unsigned input pointing to a UTXO locked +// using a P2TR script and intended to be spent using the Taproot key path. +// +// The script's x-only key is treated as the final Taproot output key. The +// builder does not apply a BIP-341/BIP-86 tap tweak during signing; callers +// must ensure the FROST signer can produce signatures for the exact output key +// committed to by the scriptPubKey. +func (tb *TransactionBuilder) AddTaprootKeyPathInput( + utxo *UnspentTransactionOutput, +) error { + utxoScript, err := tb.getScript(utxo) + if err != nil { + return fmt.Errorf( + "cannot get locking script for UTXO pointed "+ + "by the input: [%v]", + err, + ) + } + + scriptType := GetScriptType(utxoScript) + if scriptType != P2TRScript { return fmt.Errorf( - "UTXO pointed by the input is not P2PKH/P2WPKH", + "UTXO pointed by the input is not P2TR", ) } - // The UTXO was locked using a P2PKH/P2WPKH script so, the scriptCode - // required to build the sighash is equivalent to that script. Worth - // noting that the P2WPKH script is actually converted to the P2PKH script - // when used as a scriptCode, according to BIP-0143. For reference see, + return tb.addDirectKeySpendInput(utxo, utxoScript, scriptType, nil) +} + +// AddTaprootKeyPathInputWithMerkleRoot adds an unsigned input pointing to a +// UTXO locked using a BIP-341 tweaked P2TR output key and intended to be spent +// using the Taproot key path. +// +// The provided internal key and script merkle root must derive the output key +// committed to by the UTXO script. The merkle root is retained as signing +// metadata so the FROST signer can produce a key-path signature under the same +// Taproot tweak. +func (tb *TransactionBuilder) AddTaprootKeyPathInputWithMerkleRoot( + utxo *UnspentTransactionOutput, + internalKey [32]byte, + merkleRoot [32]byte, +) error { + utxoScript, err := tb.getScript(utxo) + if err != nil { + return fmt.Errorf( + "cannot get locking script for UTXO pointed "+ + "by the input: [%v]", + err, + ) + } + + scriptType := GetScriptType(utxoScript) + if scriptType != P2TRScript { + return fmt.Errorf( + "UTXO pointed by the input is not P2TR", + ) + } + + outputKey, err := ExtractTaprootKey(utxoScript) + if err != nil { + return fmt.Errorf("cannot extract taproot output key: [%v]", err) + } + + expectedOutputKey, err := TaprootOutputKey(internalKey, &merkleRoot) + if err != nil { + return fmt.Errorf("cannot derive taproot output key: [%v]", err) + } + + if !bytes.Equal(outputKey[:], expectedOutputKey[:]) { + return fmt.Errorf( + "taproot output key does not match internal key and merkle root", + ) + } + + return tb.addDirectKeySpendInput(utxo, utxoScript, scriptType, &merkleRoot) +} + +// TaprootKeyPathInputMerkleRoots returns per-input Taproot script merkle roots +// retained by the builder. The returned slice is aligned with transaction +// inputs. Non-Taproot inputs and untweaked Taproot inputs have nil entries. +func (tb *TransactionBuilder) TaprootKeyPathInputMerkleRoots() []*[32]byte { + merkleRoots := make([]*[32]byte, len(tb.sigHashArgs)) + + for i, sigHashArgs := range tb.sigHashArgs { + if sigHashArgs.taprootMerkleRoot == nil { + continue + } + + merkleRoots[i] = new([32]byte) + copy(merkleRoots[i][:], sigHashArgs.taprootMerkleRoot[:]) + } + + return merkleRoots +} + +func (tb *TransactionBuilder) addDirectKeySpendInput( + utxo *UnspentTransactionOutput, + utxoScript Script, + scriptType ScriptType, + taprootMerkleRoot *[32]byte, +) error { + // The UTXO was locked using a direct key-spend script, so the scriptCode + // required to build the sighash is equivalent to that script. Worth noting + // that the P2WPKH script is actually converted to the P2PKH script when + // used as a scriptCode, according to BIP-0143. For reference see, // https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#specification. // That conversion is handled within the `txscript.CalcWitnessSigHash` call. sigHashArgs := &inputSigHashArgs{ - value: utxo.Value, - scriptCode: utxoScript, - witness: txscript.IsWitnessProgram(utxoScript), + value: utxo.Value, + publicKeyScript: utxoScript, + scriptCode: utxoScript, + scriptType: scriptType, + taprootMerkleRoot: taprootMerkleRoot, + witness: scriptType == P2WPKHScript || scriptType == P2TRScript, } hash := chainhash.Hash(utxo.Outpoint.TransactionHash) @@ -95,10 +241,10 @@ func (tb *TransactionBuilder) AddScriptHashInput( ) } - class := txscript.GetScriptClass(utxoScript) - isPublicKeyHashScript := class == txscript.ScriptHashTy || - class == txscript.WitnessV0ScriptHashTy - if !isPublicKeyHashScript { + scriptType := GetScriptType(utxoScript) + isScriptHashScript := scriptType == P2SHScript || + scriptType == P2WSHScript + if !isScriptHashScript { return fmt.Errorf( "UTXO pointed by the input is not P2SH/P2WSH", ) @@ -108,9 +254,11 @@ func (tb *TransactionBuilder) AddScriptHashInput( // to build the sighash is equivalent to the plain-text redeem script whose // hash is included in the P2SH/P2WSH script. sigHashArgs := &inputSigHashArgs{ - value: utxo.Value, - scriptCode: redeemScript, - witness: txscript.IsWitnessProgram(utxoScript), + value: utxo.Value, + publicKeyScript: utxoScript, + scriptCode: redeemScript, + scriptType: scriptType, + witness: scriptType == P2WSHScript, } hash := chainhash.Hash(utxo.Outpoint.TransactionHash) @@ -179,13 +327,34 @@ func (tb *TransactionBuilder) ComputeSignatureHashes() ([]*big.Int, error) { // sighash fragments can be pre-computed upfront and reused. witnessSigHashFragments := txscript.NewTxSigHashes(tb.internal.MsgTx) + var taprootSigHashMidstate *taprootSignatureHashMidstate + if tb.HasTaprootKeyPathInputs() { + var err error + taprootSigHashMidstate, err = tb.taprootSignatureHashMidstate( + tb.internal.MsgTx, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot calculate taproot sighash midstate: [%v]", + err, + ) + } + } + for i := range tb.internal.TxIn { sigHashArgs := tb.sigHashArgs[i] var sigHashBytes []byte var err error - if sigHashArgs.witness { + switch sigHashArgs.scriptType { + case P2TRScript: + sigHashBytes, err = tb.calcTaprootKeyPathSignatureHash( + tb.internal.MsgTx, + i, + taprootSigHashMidstate, + ) + case P2WPKHScript, P2WSHScript: sigHashBytes, err = txscript.CalcWitnessSigHash( sigHashArgs.scriptCode, witnessSigHashFragments, @@ -194,7 +363,7 @@ func (tb *TransactionBuilder) ComputeSignatureHashes() ([]*big.Int, error) { i, sigHashArgs.value, ) - } else { + default: sigHashBytes, err = txscript.CalcSignatureHash( sigHashArgs.scriptCode, txscript.SigHashAll, @@ -246,6 +415,14 @@ func (tb *TransactionBuilder) AddSignatures( for i, input := range tb.internal.TxIn { signature := signatures[i] + sigHashArgs := tb.sigHashArgs[i] + + if sigHashArgs.scriptType == P2TRScript { + return nil, fmt.Errorf( + "input [%v] is P2TR; use AddTaprootKeyPathSignatures", + i, + ) + } // Make a sanity check to avoid producing crap transactions. if !ecdsa.Verify( @@ -265,8 +442,6 @@ func (tb *TransactionBuilder) AddSignatures( signature.PublicKey, ).SerializeCompressed() - sigHashArgs := tb.sigHashArgs[i] - if sigHashArgs.witness { witness := wire.TxWitness{ signatureBytes, @@ -309,6 +484,81 @@ func (tb *TransactionBuilder) AddSignatures( return tb.internal.toTransaction(), nil } +// SchnorrSignatureContainer is a helper type holding a serialized 64-byte +// BIP-340 Schnorr signature. +type SchnorrSignatureContainer struct { + Signature [64]byte +} + +// AddTaprootKeyPathSignatures adds Schnorr signature data for P2TR key-path +// transaction inputs and returns a signed Transaction instance. +func (tb *TransactionBuilder) AddTaprootKeyPathSignatures( + signatures []*SchnorrSignatureContainer, +) (*Transaction, error) { + if len(tb.sigHashes) == 0 { + return nil, fmt.Errorf("signature hashes must be computed first") + } + + if len(signatures) != len(tb.internal.TxIn) { + return nil, fmt.Errorf("wrong signatures count") + } + + if !tb.HasOnlyTaprootKeyPathInputs() { + return nil, fmt.Errorf( + "taproot key-path signatures require all inputs to be P2TR", + ) + } + + for i, input := range tb.internal.TxIn { + signature := signatures[i] + if signature == nil { + return nil, fmt.Errorf("signature for input [%v] is nil", i) + } + + signatureBytes := make([]byte, len(signature.Signature)) + copy(signatureBytes, signature.Signature[:]) + + taprootKey, err := ExtractTaprootKey(tb.sigHashArgs[i].publicKeyScript) + if err != nil { + return nil, fmt.Errorf( + "cannot extract taproot key for input [%v]: [%v]", + i, + err, + ) + } + + taprootPublicKey, err := schnorr.ParsePubKey(taprootKey[:]) + if err != nil { + return nil, fmt.Errorf( + "cannot parse taproot key for input [%v]: [%v]", + i, + err, + ) + } + + taprootSignature, err := schnorr.ParseSignature(signatureBytes) + if err != nil { + return nil, fmt.Errorf( + "cannot parse taproot key-path signature for input [%v]: [%v]", + i, + err, + ) + } + + sigHashBytes := tb.sigHashes[i].FillBytes(make([]byte, sha256.Size)) + if !taprootSignature.Verify(sigHashBytes, taprootPublicKey) { + return nil, fmt.Errorf( + "invalid taproot key-path signature for input [%v]", + i, + ) + } + + input.Witness = wire.TxWitness{signatureBytes} + } + + return tb.internal.toTransaction(), nil +} + // TotalInputsValue returns the total value of transaction inputs. func (tb *TransactionBuilder) TotalInputsValue() int64 { totalInputsValue := int64(0) @@ -320,15 +570,408 @@ 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 { + previousInput := previousInputs[i] + replacedInput := replacedInternal.TxIn[i] + + if previousInput == nil || replacedInput == nil { + 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, + ) + } + + // The replacement's SignatureScript and Witness are both empty here + // because of the two refusals above, so the per-input restore below + // only has to decide what to copy *from* the previous input. + if tb.sigHashArgs[i].witness { + // Witness inputs may carry a single-element pre-signing witness + // that holds a P2WSH-style redeem script. Multi-element witnesses + // belong to P2TR script-path spends or other workflows that are + // not in scope for the current FROST migration, and silently + // dropping them produced malformed transactions later — refuse + // instead so the unsupported case fails loudly. Lifting this to + // support multi-element witnesses requires a per-input policy + // rather than a blanket copy because the replacement could + // legitimately differ in witness shape from the previous input. + switch len(previousInput.Witness) { + case 0: + // Nothing to restore (typical P2TR key-path or P2WPKH). + case 1: + redeemScript := append([]byte{}, previousInput.Witness[0]...) + replacedInput.Witness = wire.TxWitness{redeemScript} + default: + return fmt.Errorf( + "replacement transaction input [%d] previous witness has "+ + "[%d] elements; only zero- or single-element "+ + "pre-signing witnesses are currently supported for "+ + "restoration", + i, + len(previousInput.Witness), + ) + } + } else if len(previousInput.SignatureScript) > 0 { + replacedInput.SignatureScript = append( + []byte{}, + previousInput.SignatureScript..., + ) + } + } + + tb.internal = replacedInternal + tb.sigHashes = nil + + 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 { + 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{ + // 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), + }, + ) + } + + 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 +} + +func (tb *TransactionBuilder) calcTaprootKeyPathSignatureHash( + tx *wire.MsgTx, + inputIndex int, + midstate *taprootSignatureHashMidstate, +) ([]byte, error) { + if tx == nil { + return nil, fmt.Errorf("transaction is nil") + } + if midstate == nil { + return nil, fmt.Errorf("taproot sighash midstate is nil") + } + + if inputIndex < 0 || inputIndex >= len(tx.TxIn) { + return nil, fmt.Errorf( + "input index [%d] out of range for [%d] inputs", + inputIndex, + len(tx.TxIn), + ) + } + + if len(tx.TxIn) != len(tb.sigHashArgs) { + return nil, fmt.Errorf( + "input metadata mismatch: [%d] tx inputs, [%d] sighash args", + len(tx.TxIn), + len(tb.sigHashArgs), + ) + } + + var sigMsg bytes.Buffer + + // BIP-341 defines the final digest as tagged_hash("TapSighash", + // 0x00 || SigMsg(0x00, 0)). The first byte is the epoch and the second + // byte is SIGHASH_DEFAULT. + sigMsg.WriteByte(0x00) + sigMsg.WriteByte(0x00) + + if err := binary.Write(&sigMsg, binary.LittleEndian, tx.Version); err != nil { + return nil, err + } + if err := binary.Write(&sigMsg, binary.LittleEndian, tx.LockTime); err != nil { + return nil, err + } + + sigMsg.Write(midstate.hashPrevOuts[:]) + sigMsg.Write(midstate.hashInputAmounts[:]) + sigMsg.Write(midstate.hashInputScripts[:]) + sigMsg.Write(midstate.hashSequences[:]) + sigMsg.Write(midstate.hashOutputs[:]) + + // Key-path spends use ext_flag=0 and this implementation does not attach + // a Taproot annex, so spend_type is 0. + sigMsg.WriteByte(0x00) + + if err := binary.Write( + &sigMsg, + binary.LittleEndian, + uint32(inputIndex), + ); err != nil { + return nil, err + } + + hash := chainhash.TaggedHash([]byte("TapSighash"), sigMsg.Bytes()) + return hash.CloneBytes(), nil +} + +type taprootSignatureHashMidstate struct { + hashPrevOuts [chainhash.HashSize]byte + hashInputAmounts [chainhash.HashSize]byte + hashInputScripts [chainhash.HashSize]byte + hashSequences [chainhash.HashSize]byte + hashOutputs [chainhash.HashSize]byte +} + +func (tb *TransactionBuilder) taprootSignatureHashMidstate( + tx *wire.MsgTx, +) (*taprootSignatureHashMidstate, error) { + if tx == nil { + return nil, fmt.Errorf("transaction is nil") + } + + if len(tx.TxIn) != len(tb.sigHashArgs) { + return nil, fmt.Errorf( + "input metadata mismatch: [%d] tx inputs, [%d] sighash args", + len(tx.TxIn), + len(tb.sigHashArgs), + ) + } + + hashPrevOuts, err := tb.taprootHashPrevOuts(tx) + if err != nil { + return nil, err + } + + hashInputAmounts, err := tb.taprootHashInputAmounts() + if err != nil { + return nil, err + } + + hashInputScripts, err := tb.taprootHashInputScripts() + if err != nil { + return nil, err + } + + hashSequences, err := tb.taprootHashSequences(tx) + if err != nil { + return nil, err + } + + hashOutputs, err := tb.taprootHashOutputs(tx) + if err != nil { + return nil, err + } + + return &taprootSignatureHashMidstate{ + hashPrevOuts: hashPrevOuts, + hashInputAmounts: hashInputAmounts, + hashInputScripts: hashInputScripts, + hashSequences: hashSequences, + hashOutputs: hashOutputs, + }, nil +} + +func (tb *TransactionBuilder) taprootHashPrevOuts( + tx *wire.MsgTx, +) ([chainhash.HashSize]byte, error) { + var buffer bytes.Buffer + for _, input := range tx.TxIn { + if err := writeOutPoint(&buffer, &input.PreviousOutPoint); err != nil { + return [chainhash.HashSize]byte{}, err + } + } + + return chainhash.HashH(buffer.Bytes()), nil +} + +func (tb *TransactionBuilder) taprootHashInputAmounts() ( + [chainhash.HashSize]byte, + error, +) { + var buffer bytes.Buffer + for i, sigHashArgs := range tb.sigHashArgs { + if sigHashArgs.value < 0 { + return [chainhash.HashSize]byte{}, fmt.Errorf( + "input [%d] value is negative", + i, + ) + } + + if err := binary.Write( + &buffer, + binary.LittleEndian, + uint64(sigHashArgs.value), + ); err != nil { + return [chainhash.HashSize]byte{}, err + } + } + + return chainhash.HashH(buffer.Bytes()), nil +} + +func (tb *TransactionBuilder) taprootHashInputScripts() ( + [chainhash.HashSize]byte, + error, +) { + var buffer bytes.Buffer + for i, sigHashArgs := range tb.sigHashArgs { + if err := wire.WriteVarBytes( + &buffer, + 0, + sigHashArgs.publicKeyScript, + ); err != nil { + return [chainhash.HashSize]byte{}, fmt.Errorf( + "cannot write public key script for input [%d]: [%v]", + i, + err, + ) + } + } + + return chainhash.HashH(buffer.Bytes()), nil +} + +func (tb *TransactionBuilder) taprootHashSequences( + tx *wire.MsgTx, +) ([chainhash.HashSize]byte, error) { + var buffer bytes.Buffer + for _, input := range tx.TxIn { + if err := binary.Write( + &buffer, + binary.LittleEndian, + input.Sequence, + ); err != nil { + return [chainhash.HashSize]byte{}, err + } + } + + return chainhash.HashH(buffer.Bytes()), nil +} + +func (tb *TransactionBuilder) taprootHashOutputs( + tx *wire.MsgTx, +) ([chainhash.HashSize]byte, error) { + var buffer bytes.Buffer + for i, output := range tx.TxOut { + if err := wire.WriteTxOut(&buffer, 0, 0, output); err != nil { + return [chainhash.HashSize]byte{}, fmt.Errorf( + "cannot write output [%d]: [%v]", + i, + err, + ) + } + } + + return chainhash.HashH(buffer.Bytes()), nil +} + +func writeOutPoint(buffer *bytes.Buffer, outpoint *wire.OutPoint) error { + if _, err := buffer.Write(outpoint.Hash[:]); err != nil { + return err + } + + return binary.Write(buffer, binary.LittleEndian, outpoint.Index) +} + // inputSigHashArgs is a helper structure holding some arguments required to // compute a sighash for the given input. type inputSigHashArgs struct { // value denotes the satoshi value of the UTXO pointed by the given input. value int64 + // publicKeyScript is the locking script of the UTXO pointed by the given + // input. + publicKeyScript []byte // scriptCode is a component of the input's sighash and is the script that // is actually executed while unlocking the given UTXO. The scriptCode // depends on the script type that was used to lock the given UTXO. scriptCode []byte + // scriptType denotes the locking script type of the UTXO pointed by the + // given input. + scriptType ScriptType + // taprootMerkleRoot denotes the BIP-341 script merkle root used to tweak + // the P2TR input's output key. It is nil for untweaked P2TR inputs and + // non-Taproot inputs. + taprootMerkleRoot *[32]byte // witness denotes whether the given input point's to a UTXO locked using // a witness script. witness bool diff --git a/pkg/bitcoin/transaction_builder_test.go b/pkg/bitcoin/transaction_builder_test.go index 96adf8dede..74335991ef 100644 --- a/pkg/bitcoin/transaction_builder_test.go +++ b/pkg/bitcoin/transaction_builder_test.go @@ -7,6 +7,10 @@ import ( "strings" "testing" + btcec2 "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" "github.com/keep-network/keep-core/internal/testutils" ) @@ -111,6 +115,267 @@ func TestTransactionBuilder_AddPublicKeyHashInput(t *testing.T) { } } +func TestTransactionBuilder_AddPublicKeyHashInput_AcceptsTaprootKeyPathInputForBackwardCompatibility( + t *testing.T, +) { + localChain := newLocalChain() + builder := NewTransactionBuilder(localChain) + + var taprootOutputKey [32]byte + copy( + taprootOutputKey[:], + hexToSlice( + t, + "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + ), + ) + + lockingScript, err := PayToTaproot(taprootOutputKey) + if err != nil { + t.Fatal(err) + } + + inputTransaction := &Transaction{ + Version: 1, + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash{0x01}, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*TransactionOutput{ + { + Value: 100000, + PublicKeyScript: lockingScript, + }, + }, + Locktime: 0, + } + + if err := localChain.addTransaction(inputTransaction); err != nil { + t.Fatal(err) + } + + inputTransactionUtxo := &UnspentTransactionOutput{ + Outpoint: &TransactionOutpoint{ + TransactionHash: inputTransaction.Hash(), + OutputIndex: 0, + }, + Value: 100000, + } + + if err := builder.AddPublicKeyHashInput(inputTransactionUtxo); err != nil { + t.Fatal(err) + } + + if !builder.HasTaprootKeyPathInputs() { + t.Fatal("expected builder to have taproot key-path inputs") + } + if !builder.HasOnlyTaprootKeyPathInputs() { + t.Fatal("expected builder to have only taproot key-path inputs") + } + + assertSigHashArgs( + t, + &inputSigHashArgs{ + value: inputTransactionUtxo.Value, + publicKeyScript: lockingScript, + scriptCode: lockingScript, + scriptType: P2TRScript, + witness: true, + }, + builder.sigHashArgs[0], + ) + assertInternalInput(t, builder, 0, &TransactionInput{ + Outpoint: inputTransactionUtxo.Outpoint, + SignatureScript: nil, + Witness: nil, + Sequence: 0xffffffff, + }) +} + +func TestTransactionBuilder_AddTaprootKeyPathInputWithMerkleRoot(t *testing.T) { + localChain := newLocalChain() + builder := NewTransactionBuilder(localChain) + + privateKey, _ := btcec2.PrivKeyFromBytes( + hexToSlice( + t, + "0101010101010101010101010101010101010101010101010101010101010101", + ), + ) + + var internalKey [32]byte + copy(internalKey[:], schnorr.SerializePubKey(privateKey.PubKey())) + + refundLeaf := Script(hexToSlice( + t, + "76a9140102030405060708090a0b0c0d0e0f101112131488ac", + )) + merkleRoot, err := TaprootLeafHash(refundLeaf) + if err != nil { + t.Fatal(err) + } + + outputKey, err := TaprootOutputKey(internalKey, &merkleRoot) + if err != nil { + t.Fatal(err) + } + + lockingScript, err := PayToTaproot(outputKey) + if err != nil { + t.Fatal(err) + } + + inputTransaction := &Transaction{ + Version: 1, + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash{0x01}, + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*TransactionOutput{ + { + Value: 100000, + PublicKeyScript: lockingScript, + }, + }, + Locktime: 0, + } + + if err := localChain.addTransaction(inputTransaction); err != nil { + t.Fatal(err) + } + + inputTransactionUtxo := &UnspentTransactionOutput{ + Outpoint: &TransactionOutpoint{ + TransactionHash: inputTransaction.Hash(), + OutputIndex: 0, + }, + Value: 100000, + } + + if err := builder.AddTaprootKeyPathInputWithMerkleRoot( + inputTransactionUtxo, + internalKey, + merkleRoot, + ); err != nil { + t.Fatal(err) + } + + assertSigHashArgs( + t, + &inputSigHashArgs{ + value: inputTransactionUtxo.Value, + publicKeyScript: lockingScript, + scriptCode: lockingScript, + scriptType: P2TRScript, + taprootMerkleRoot: &merkleRoot, + witness: true, + }, + builder.sigHashArgs[0], + ) + + merkleRoots := builder.TaprootKeyPathInputMerkleRoots() + if len(merkleRoots) != 1 { + t.Fatalf("unexpected merkle roots count: [%v]", len(merkleRoots)) + } + testutils.AssertBytesEqual(t, merkleRoot[:], merkleRoots[0][:]) +} + +func TestTransactionBuilder_AddTaprootKeyPathInputWithMerkleRootRejectsMismatch( + t *testing.T, +) { + localChain := newLocalChain() + builder := NewTransactionBuilder(localChain) + + privateKey, _ := btcec2.PrivKeyFromBytes( + hexToSlice( + t, + "0101010101010101010101010101010101010101010101010101010101010101", + ), + ) + + var internalKey [32]byte + copy(internalKey[:], schnorr.SerializePubKey(privateKey.PubKey())) + + merkleRoot, err := TaprootLeafHash(Script(hexToSlice( + t, + "76a9140102030405060708090a0b0c0d0e0f101112131488ac", + ))) + if err != nil { + t.Fatal(err) + } + + wrongMerkleRoot, err := TaprootLeafHash(Script(hexToSlice( + t, + "76a914ffffffffffffffffffffffffffffffffffffffff88ac", + ))) + if err != nil { + t.Fatal(err) + } + + outputKey, err := TaprootOutputKey(internalKey, &merkleRoot) + if err != nil { + t.Fatal(err) + } + + lockingScript, err := PayToTaproot(outputKey) + if err != nil { + t.Fatal(err) + } + + inputTransaction := &Transaction{ + Version: 1, + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash{0x01}, + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*TransactionOutput{ + { + Value: 100000, + PublicKeyScript: lockingScript, + }, + }, + Locktime: 0, + } + + if err := localChain.addTransaction(inputTransaction); err != nil { + t.Fatal(err) + } + + err = builder.AddTaprootKeyPathInputWithMerkleRoot( + &UnspentTransactionOutput{ + Outpoint: &TransactionOutpoint{ + TransactionHash: inputTransaction.Hash(), + OutputIndex: 0, + }, + Value: 100000, + }, + internalKey, + wrongMerkleRoot, + ) + if err == nil { + t.Fatal("expected taproot output key mismatch error") + } + if !strings.Contains(err.Error(), "taproot output key does not match") { + t.Fatalf("unexpected error: [%v]", err) + } +} + func TestTransactionBuilder_AddInputReturnsErrorForOutOfRangeOutputIndex( t *testing.T, ) { @@ -245,6 +510,744 @@ func TestTransactionBuilder_AddOutput(t *testing.T) { assertInternalOutput(t, builder, 0, output) } +func TestTransactionBuilder_AddTaprootKeyPathSignatures(t *testing.T) { + localChain := newLocalChain() + builder := NewTransactionBuilder(localChain) + + privateKeyBytes := hexToSlice( + t, + "0101010101010101010101010101010101010101010101010101010101010101", + ) + privateKey, publicKey := btcec2.PrivKeyFromBytes(privateKeyBytes) + + var taprootOutputKey [32]byte + copy(taprootOutputKey[:], schnorr.SerializePubKey(publicKey)) + + inputScript, err := PayToTaproot(taprootOutputKey) + if err != nil { + t.Fatal(err) + } + + var outputPublicKeyHash [20]byte + copy( + outputPublicKeyHash[:], + hexToSlice(t, "0202020202020202020202020202020202020202"), + ) + outputScript, err := PayToWitnessPublicKeyHash(outputPublicKeyHash) + if err != nil { + t.Fatal(err) + } + + previousTransaction := &Transaction{ + Version: 1, + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash{ + 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, + }, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*TransactionOutput{ + { + Value: 100000, + PublicKeyScript: inputScript, + }, + }, + Locktime: 0, + } + + if err := localChain.addTransaction(previousTransaction); err != nil { + t.Fatal(err) + } + + err = builder.AddTaprootKeyPathInput(&UnspentTransactionOutput{ + Outpoint: &TransactionOutpoint{ + TransactionHash: previousTransaction.Hash(), + OutputIndex: 0, + }, + Value: 100000, + }) + if err != nil { + t.Fatal(err) + } + + builder.AddOutput(&TransactionOutput{ + Value: 90000, + PublicKeyScript: outputScript, + }) + + if !builder.HasTaprootKeyPathInputs() { + t.Fatal("expected builder to have taproot key-path inputs") + } + if !builder.HasOnlyTaprootKeyPathInputs() { + t.Fatal("expected builder to have only taproot key-path inputs") + } + + sigHashes, err := builder.ComputeSignatureHashes() + if err != nil { + t.Fatal(err) + } + + testutils.AssertIntsEqual(t, "signature hashes count", 1, len(sigHashes)) + + expectedSigHash := hexToSlice( + t, + "96653d19d603d309d22cfe2ccd0ba445e40629dea18d46108caa601055ec4318", + ) + // This vector was generated with btcd v0.23.4's BIP-341 + // CalcTaprootSignatureHash implementation and independently + // cross-checked by reviewers. + sigHashBytes := sigHashes[0].FillBytes(make([]byte, 32)) + testutils.AssertBytesEqual(t, expectedSigHash, sigHashBytes) + + signature, err := schnorr.Sign(privateKey, sigHashBytes) + if err != nil { + t.Fatal(err) + } + signatureBytes := signature.Serialize() + + expectedSignature := hexToSlice( + t, + "5e847a0c22486f3b89ff80edd5afaf4be550aa411a0a7e28cff19d2b5924d77102bbf9a0a51100f4fdfc8435d0e8ff0f61dfdeccd464b78c553b1b4414ac0877", + ) + testutils.AssertBytesEqual(t, expectedSignature, signatureBytes) + + var signatureContainer [64]byte + copy(signatureContainer[:], signatureBytes) + + transaction, err := builder.AddTaprootKeyPathSignatures( + []*SchnorrSignatureContainer{ + { + Signature: signatureContainer, + }, + }, + ) + if err != nil { + t.Fatal(err) + } + + testutils.AssertIntsEqual( + t, + "transaction inputs count", + 1, + len(transaction.Inputs), + ) + testutils.AssertIntsEqual( + t, + "taproot witness elements count", + 1, + len(transaction.Inputs[0].Witness), + ) + testutils.AssertBytesEqual( + t, + expectedSignature, + transaction.Inputs[0].Witness[0], + ) + testutils.AssertBytesEqual(t, nil, transaction.Inputs[0].SignatureScript) +} + +func TestTransactionBuilder_AddTaprootKeyPathSignatures_MultipleInputs( + t *testing.T, +) { + localChain := newLocalChain() + builder := NewTransactionBuilder(localChain) + + privateKeyBytes := hexToSlice( + t, + "0101010101010101010101010101010101010101010101010101010101010101", + ) + privateKey, publicKey := btcec2.PrivKeyFromBytes(privateKeyBytes) + + var taprootOutputKey [32]byte + copy(taprootOutputKey[:], schnorr.SerializePubKey(publicKey)) + + inputScript, err := PayToTaproot(taprootOutputKey) + if err != nil { + t.Fatal(err) + } + + var outputPublicKeyHash [20]byte + copy( + outputPublicKeyHash[:], + hexToSlice(t, "0202020202020202020202020202020202020202"), + ) + outputScript, err := PayToWitnessPublicKeyHash(outputPublicKeyHash) + if err != nil { + t.Fatal(err) + } + + inputValues := []int64{100000, 110000} + for i, value := range inputValues { + previousTransaction := &Transaction{ + Version: 1, + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash{byte(0x20 + i)}, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*TransactionOutput{ + { + Value: value, + PublicKeyScript: inputScript, + }, + }, + Locktime: 0, + } + + if err := localChain.addTransaction(previousTransaction); err != nil { + t.Fatal(err) + } + + err = builder.AddTaprootKeyPathInput(&UnspentTransactionOutput{ + Outpoint: &TransactionOutpoint{ + TransactionHash: previousTransaction.Hash(), + OutputIndex: 0, + }, + Value: value, + }) + if err != nil { + t.Fatal(err) + } + } + + builder.AddOutput(&TransactionOutput{ + Value: 209000, + PublicKeyScript: outputScript, + }) + + sigHashes, err := builder.ComputeSignatureHashes() + if err != nil { + t.Fatal(err) + } + + testutils.AssertIntsEqual(t, "signature hashes count", 2, len(sigHashes)) + if sigHashes[0].Cmp(sigHashes[1]) == 0 { + t.Fatal("expected distinct taproot signature hashes") + } + + signatures := make([]*SchnorrSignatureContainer, len(sigHashes)) + expectedSignatures := make([][]byte, len(sigHashes)) + for i, sigHash := range sigHashes { + signature, err := schnorr.Sign( + privateKey, + sigHash.FillBytes(make([]byte, 32)), + ) + if err != nil { + t.Fatal(err) + } + + signatureBytes := signature.Serialize() + expectedSignatures[i] = signatureBytes + + var signatureContainer [64]byte + copy(signatureContainer[:], signatureBytes) + signatures[i] = &SchnorrSignatureContainer{ + Signature: signatureContainer, + } + } + + transaction, err := builder.AddTaprootKeyPathSignatures(signatures) + if err != nil { + t.Fatal(err) + } + + testutils.AssertIntsEqual( + t, + "transaction inputs count", + 2, + len(transaction.Inputs), + ) + for i, input := range transaction.Inputs { + testutils.AssertIntsEqual( + t, + fmt.Sprintf("taproot witness elements count for input [%d]", i), + 1, + len(input.Witness), + ) + testutils.AssertBytesEqual(t, expectedSignatures[i], input.Witness[0]) + testutils.AssertBytesEqual(t, nil, input.SignatureScript) + } +} + +func TestTransactionBuilder_AddSignaturesRejectsTaprootInput(t *testing.T) { + localChain := newLocalChain() + builder := NewTransactionBuilder(localChain) + + var taprootOutputKey [32]byte + copy( + taprootOutputKey[:], + hexToSlice( + t, + "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + ), + ) + inputScript, err := PayToTaproot(taprootOutputKey) + if err != nil { + t.Fatal(err) + } + + previousTransaction := &Transaction{ + Version: 1, + Inputs: []*TransactionInput{ + { + Outpoint: &TransactionOutpoint{ + TransactionHash: Hash{0x01}, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*TransactionOutput{ + { + Value: 100000, + PublicKeyScript: inputScript, + }, + }, + Locktime: 0, + } + + if err := localChain.addTransaction(previousTransaction); err != nil { + t.Fatal(err) + } + + err = builder.AddTaprootKeyPathInput(&UnspentTransactionOutput{ + Outpoint: &TransactionOutpoint{ + TransactionHash: previousTransaction.Hash(), + OutputIndex: 0, + }, + Value: 100000, + }) + if err != nil { + t.Fatal(err) + } + + var outputPublicKeyHash [20]byte + outputScript, err := PayToWitnessPublicKeyHash(outputPublicKeyHash) + if err != nil { + t.Fatal(err) + } + builder.AddOutput(&TransactionOutput{ + Value: 90000, + PublicKeyScript: outputScript, + }) + + if _, err := builder.ComputeSignatureHashes(); err != nil { + t.Fatal(err) + } + + _, err = builder.AddSignatures([]*SignatureContainer{ + { + R: big.NewInt(1), + S: big.NewInt(1), + }, + }) + if err == nil { + t.Fatal("expected AddSignatures to reject a taproot input") + } + if !strings.Contains(err.Error(), "use AddTaprootKeyPathSignatures") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +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_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_ReplaceUnsignedTransaction_RejectsMultiElementPreviousWitness( + t *testing.T, +) { + builder := NewTransactionBuilder(nil) + + var txHash chainhash.Hash + previousInput := wire.NewTxIn(wire.NewOutPoint(&txHash, 0), nil, nil) + // Pre-signing witness that mimics a P2TR script-path spend: [script, + // controlBlock]. The restoration path supports only zero- or + // single-element previous witnesses today; the multi-element case must + // fail loudly rather than silently dropping data later in signing. + previousInput.Witness = wire.TxWitness{ + []byte{0x51, 0x52}, + []byte{0xc0, 0xab, 0xcd}, + } + builder.internal.AddTxIn(previousInput) + 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, + }, + Sequence: 0xffffffff, + }, + }, + }, + ) + if err == nil { + t.Fatal("expected multi-element witness restoration error") + } + + if !strings.Contains( + err.Error(), + "previous witness has [2] elements", + ) { + t.Fatalf("unexpected error: [%v]", err) + } + if !strings.Contains( + err.Error(), + "only zero- or single-element", + ) { + t.Fatalf("unexpected error: [%v]", err) + } +} + +func TestTransactionBuilder_UnsignedTransactionIO(t *testing.T) { + builder := NewTransactionBuilder(nil) + + var txHash chainhash.Hash + for i := range txHash { + 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}) + 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 != expectedTxIDHex { + t.Fatalf( + "unexpected input txid\nexpected: [%v]\nactual: [%v]", + expectedTxIDHex, + 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 @@ -473,6 +1476,37 @@ func assertSigHashArgs(t *testing.T, expected, actual *inputSigHashArgs) { actual.scriptCode, ) + if expected.publicKeyScript != nil { + testutils.AssertBytesEqual( + t, + expected.publicKeyScript, + actual.publicKeyScript, + ) + } + + if expected.scriptType != NonStandardScript { + testutils.AssertIntsEqual( + t, + "sighash args script type", + int(expected.scriptType), + int(actual.scriptType), + ) + } + + if expected.taprootMerkleRoot != nil { + if actual.taprootMerkleRoot == nil { + t.Fatal("expected taproot merkle root") + } + + testutils.AssertBytesEqual( + t, + expected.taprootMerkleRoot[:], + actual.taprootMerkleRoot[:], + ) + } else if actual.taprootMerkleRoot != nil { + t.Fatal("unexpected taproot merkle root") + } + testutils.AssertBoolsEqual( t, "sighash args witness flag", diff --git a/pkg/chain/ethereum/ethereum.go b/pkg/chain/ethereum/ethereum.go index 57b800edb0..2e42461e42 100644 --- a/pkg/chain/ethereum/ethereum.go +++ b/pkg/chain/ethereum/ethereum.go @@ -339,18 +339,18 @@ func (bc *baseChain) OperatorKeyPair() ( func (bc *baseChain) GetBlockNumberByTimestamp( timestamp uint64, ) (uint64, error) { - block, err := bc.currentBlock() + block, err := bc.currentBlockHeader() if err != nil { return 0, fmt.Errorf("cannot get current block: [%v]", err) } - if block.Time() < timestamp { + if block.Time < timestamp { return 0, fmt.Errorf("requested timestamp is in the future") } // Corner case shortcut. - if block.Time() == timestamp { - return block.NumberU64(), nil + if block.Time == timestamp { + return block.Number.Uint64(), nil } // The Ethereum average block time (https://etherscan.io/chart/blocktime) @@ -366,9 +366,9 @@ func (bc *baseChain) GetBlockNumberByTimestamp( // the better one. const averageBlockTime = 13 - for block.Time() > timestamp { + for block.Time > timestamp { // timeDiff is always >0 due to the for-loop condition. - timeDiff := block.Time() - timestamp + timeDiff := block.Time - timestamp // blockDiff is an integer whose value can be: // - >=1 if timeDiff >= averageBlockTime // - ==0 if timeDiff < averageBlockTime @@ -380,7 +380,7 @@ func (bc *baseChain) GetBlockNumberByTimestamp( break } - block, err = bc.blockByNumber(block.NumberU64() - blockDiff) + block, err = bc.headerByNumber(block.Number.Uint64() - blockDiff) if err != nil { return 0, fmt.Errorf("cannot get block: [%v]", err) } @@ -393,8 +393,8 @@ func (bc *baseChain) GetBlockNumberByTimestamp( // // First, try to reduce Case 1 by walking forward block by block until // we achieve Case 2 or 3. - for block.Time() < timestamp { - block, err = bc.blockByNumber(block.NumberU64() + 1) + for block.Time < timestamp { + block, err = bc.headerByNumber(block.Number.Uint64() + 1) if err != nil { return 0, fmt.Errorf("cannot get block: [%v]", err) } @@ -402,16 +402,16 @@ func (bc *baseChain) GetBlockNumberByTimestamp( // At this point, only Case 2 or 3 are possible. If we have Case 2, // just get the previous block and compare which one lies closer to // the requested timestamp. - if block.Time() > timestamp { - previousBlock, err := bc.blockByNumber(block.NumberU64() - 1) + if block.Time > timestamp { + previousBlock, err := bc.headerByNumber(block.Number.Uint64() - 1) if err != nil { return 0, fmt.Errorf("cannot get block: [%v]", err) } - return closerBlock(timestamp, previousBlock, block).NumberU64(), nil + return closerBlock(timestamp, previousBlock, block).Number.Uint64(), nil } - return block.NumberU64(), nil + return block.Number.Uint64(), nil } // GetBlockHashByNumber gets the block hash for the given block number. @@ -427,14 +427,14 @@ func (bc *baseChain) GetBlockHashByNumber(blockNumber uint64) ( return header.Hash(), nil } -// currentBlock fetches the current block. -func (bc *baseChain) currentBlock() (*types.Block, error) { +// currentBlockHeader fetches the current block header. +func (bc *baseChain) currentBlockHeader() (*types.Header, error) { currentBlockNumber, err := bc.blockCounter.CurrentBlock() if err != nil { return nil, err } - currentBlock, err := bc.blockByNumber(currentBlockNumber) + currentBlock, err := bc.headerByNumber(currentBlockNumber) if err != nil { return nil, err } @@ -442,15 +442,6 @@ func (bc *baseChain) currentBlock() (*types.Block, error) { return currentBlock, nil } -// blockByNumber returns the block for the given block number. Times out -// if the underlying client call takes more than 30 seconds. -func (bc *baseChain) blockByNumber(number uint64) (*types.Block, error) { - ctx, cancelCtx := context.WithTimeout(context.Background(), 30*time.Second) - defer cancelCtx() - - return bc.client.BlockByNumber(ctx, big.NewInt(int64(number))) -} - // headerByNumber returns the header for the given block number. Times out // if the underlying client call takes more than 30 seconds. func (bc *baseChain) headerByNumber(number uint64) (*types.Header, error) { @@ -463,7 +454,7 @@ func (bc *baseChain) headerByNumber(number uint64) (*types.Header, error) { // closerBlock check timestamps of blocks b1 and b2 and returns the block // whose timestamp lies closer to the requested timestamp. If the distance // is same for both blocks, the block with greater block number is returned. -func closerBlock(timestamp uint64, b1, b2 *types.Block) *types.Block { +func closerBlock(timestamp uint64, b1, b2 *types.Header) *types.Header { abs := func(x int64) int64 { if x < 0 { return -x @@ -471,12 +462,12 @@ func closerBlock(timestamp uint64, b1, b2 *types.Block) *types.Block { return x } - b1Diff := abs(int64(b1.Time() - timestamp)) - b2Diff := abs(int64(b2.Time() - timestamp)) + b1Diff := abs(int64(b1.Time - timestamp)) + b2Diff := abs(int64(b2.Time - timestamp)) // If the differences are same, return the block with greater number. if b1Diff == b2Diff { - if b2.NumberU64() > b1.NumberU64() { + if b2.Number.Uint64() > b1.Number.Uint64() { return b2 } return b1 diff --git a/pkg/chain/ethereum/ethereum_integration_test.go b/pkg/chain/ethereum/ethereum_integration_test.go index 319a84016f..b868f517b3 100644 --- a/pkg/chain/ethereum/ethereum_integration_test.go +++ b/pkg/chain/ethereum/ethereum_integration_test.go @@ -6,6 +6,7 @@ package ethereum import ( "fmt" "reflect" + "strings" "testing" "time" @@ -66,6 +67,9 @@ func TestBaseChain_GetBlockNumberByTimestamp(t *testing.T) { for testName, test := range tests { t.Run(testName, func(t *testing.T) { blockNumber, err := bc.GetBlockNumberByTimestamp(test.timestamp) + if isProviderRateLimitError(err) { + t.Skipf("skipping test due to Ethereum provider rate limit: [%v]", err) + } if !reflect.DeepEqual(err, test.expectedError) { t.Errorf( @@ -84,3 +88,7 @@ func TestBaseChain_GetBlockNumberByTimestamp(t *testing.T) { }) } } + +func isProviderRateLimitError(err error) bool { + return err != nil && strings.Contains(err.Error(), "429 Too Many Requests") +} diff --git a/pkg/chain/ethereum/frost/gen/abi/FrostWalletRegistry.go b/pkg/chain/ethereum/frost/gen/abi/FrostWalletRegistry.go new file mode 100644 index 0000000000..da872a230d --- /dev/null +++ b/pkg/chain/ethereum/frost/gen/abi/FrostWalletRegistry.go @@ -0,0 +1,6361 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package abi + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// FrostDkgParameters is an auto generated low-level Go binding around an user-defined struct. +type FrostDkgParameters struct { + SeedTimeout *big.Int + ResultChallengePeriodLength *big.Int + ResultChallengeExtraGas *big.Int + ResultSubmissionTimeout *big.Int + SubmitterPrecedencePeriodLength *big.Int +} + +// FrostDkgResult is an auto generated low-level Go binding around an user-defined struct. +type FrostDkgResult struct { + SubmitterMemberIndex *big.Int + XOnlyOutputKey [32]byte + MisbehavedMembersIndices []uint8 + Signatures []byte + SigningMembersIndices []*big.Int + Members []uint32 + MembersHash [32]byte +} + +// FrostInactivityClaim is an auto generated low-level Go binding around an user-defined struct. +type FrostInactivityClaim struct { + WalletID [32]byte + InactiveMembersIndices []*big.Int + HeartbeatFailed bool + Signatures []byte + SigningMembersIndices []*big.Int +} + +// FrostRegistryWalletsWallet is an auto generated low-level Go binding around an user-defined struct. +type FrostRegistryWalletsWallet struct { + MembersIdsHash [32]byte + XOnlyOutputKey [32]byte +} + +// FrostWalletRegistryMetaData contains all meta data concerning the FrostWalletRegistry contract. +var FrostWalletRegistryMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"contractSortitionPool\",\"name\":\"_sortitionPool\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"LifecycleOwnerNotSet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"WalletNotRegistered\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"XOnlyOutputKeyAlreadyRegistered\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"XOnlyOutputKeyIsLegacyAlias\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"XOnlyOutputKeyIsZero\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"}],\"name\":\"AuthorizationDecreaseApproved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fromAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"toAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"decreasingAt\",\"type\":\"uint64\"}],\"name\":\"AuthorizationDecreaseRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fromAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"toAmount\",\"type\":\"uint96\"}],\"name\":\"AuthorizationIncreased\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"minimumAuthorization\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"authorizationDecreaseDelay\",\"type\":\"uint64\"},{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"authorizationDecreaseChangePeriod\",\"type\":\"uint64\"}],\"name\":\"AuthorizationParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"resultHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"slashingAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"maliciousSubmitter\",\"type\":\"address\"}],\"name\":\"DkgMaliciousResultSlashed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"resultHash\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"slashingAmount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"maliciousSubmitter\",\"type\":\"address\"}],\"name\":\"DkgMaliciousResultSlashingFailed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"seedTimeout\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"resultChallengePeriodLength\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"resultChallengeExtraGas\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"resultSubmissionTimeout\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"resultSubmitterPrecedencePeriodLength\",\"type\":\"uint256\"}],\"name\":\"DkgParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"resultHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"approver\",\"type\":\"address\"}],\"name\":\"DkgResultApproved\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"resultHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"string\",\"name\":\"reason\",\"type\":\"string\"}],\"name\":\"DkgResultChallenged\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"resultHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"seed\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"indexed\":false,\"internalType\":\"structFrostDkg.Result\",\"name\":\"result\",\"type\":\"tuple\"}],\"name\":\"DkgResultSubmitted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"DkgSeedTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"seed\",\"type\":\"uint256\"}],\"name\":\"DkgStarted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"DkgStateLocked\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"DkgTimedOut\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"dkgResultSubmissionGas\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"dkgResultApprovalGasOffset\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"notifyOperatorInactivityGasOffset\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"notifySeedTimeoutGasOffset\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"notifyDkgTimeoutNegativeGasOffset\",\"type\":\"uint256\"}],\"name\":\"GasParametersUpdated\",\"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\":true,\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"notifier\",\"type\":\"address\"}],\"name\":\"InactivityClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"version\",\"type\":\"uint8\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"fromAmount\",\"type\":\"uint96\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"toAmount\",\"type\":\"uint96\"}],\"name\":\"InvoluntaryAuthorizationDecreaseFailed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"lifecycleOwner\",\"type\":\"address\"}],\"name\":\"LifecycleOwnerUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"OperatorJoinedSortitionPool\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"OperatorRegistered\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"OperatorStatusUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"randomBeacon\",\"type\":\"address\"}],\"name\":\"RandomBeaconUpgraded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"newReimbursementPool\",\"type\":\"address\"}],\"name\":\"ReimbursementPoolUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"maliciousDkgResultNotificationRewardMultiplier\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"sortitionPoolRewardsBanDuration\",\"type\":\"uint256\"}],\"name\":\"RewardParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint96\",\"name\":\"amount\",\"type\":\"uint96\"}],\"name\":\"RewardsWithdrawn\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"maliciousDkgResultSlashingAmount\",\"type\":\"uint256\"}],\"name\":\"SlashingParametersUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"}],\"name\":\"WalletClosed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"dkgResultHash\",\"type\":\"bytes32\"}],\"name\":\"WalletCreated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"walletOwner\",\"type\":\"address\"}],\"name\":\"WalletOwnerUpdated\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"relayEntry\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"__beaconCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"}],\"name\":\"approveAuthorizationDecrease\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"dkgResult\",\"type\":\"tuple\"}],\"name\":\"approveDkgResult\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"fromAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint96\",\"name\":\"toAmount\",\"type\":\"uint96\"}],\"name\":\"authorizationDecreaseRequested\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"fromAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint96\",\"name\":\"toAmount\",\"type\":\"uint96\"}],\"name\":\"authorizationIncreased\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"authorizationParameters\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"minimumAuthorization\",\"type\":\"uint96\"},{\"internalType\":\"uint64\",\"name\":\"authorizationDecreaseDelay\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"authorizationDecreaseChangePeriod\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"authorizationSource\",\"outputs\":[{\"internalType\":\"contractIFrostAuthorizationSource\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"}],\"name\":\"availableRewards\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"\",\"type\":\"uint96\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"dkgResult\",\"type\":\"tuple\"}],\"name\":\"challengeDkgResult\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"}],\"name\":\"closeWallet\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"dkgParameters\",\"outputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"seedTimeout\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"resultChallengePeriodLength\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"resultChallengeExtraGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"resultSubmissionTimeout\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"submitterPrecedencePeriodLength\",\"type\":\"uint256\"}],\"internalType\":\"structFrostDkg.Parameters\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"}],\"name\":\"eligibleStake\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"\",\"type\":\"uint96\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"gasParameters\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"dkgResultSubmissionGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"dkgResultApprovalGasOffset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"notifyOperatorInactivityGasOffset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"notifySeedTimeoutGasOffset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"notifyDkgTimeoutNegativeGasOffset\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"}],\"name\":\"getWallet\",\"outputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"membersIdsHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostRegistryWallets.Wallet\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getWalletCreationState\",\"outputs\":[{\"internalType\":\"enumFrostDkg.State\",\"name\":\"\",\"type\":\"uint8\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"}],\"name\":\"getWalletXOnlyOutputKey\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"governance\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"hasDkgTimedOut\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"hasSeedTimedOut\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"inactivityClaimNonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractFrostDkgValidator\",\"name\":\"_ecdsaDkgValidator\",\"type\":\"address\"},{\"internalType\":\"contractIRandomBeacon\",\"name\":\"_randomBeacon\",\"type\":\"address\"},{\"internalType\":\"contractReimbursementPool\",\"name\":\"_reimbursementPool\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_bridge\",\"type\":\"address\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_authorizationSource\",\"type\":\"address\"}],\"name\":\"initializeV2\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"},{\"internalType\":\"uint96\",\"name\":\"fromAmount\",\"type\":\"uint96\"},{\"internalType\":\"uint96\",\"name\":\"toAmount\",\"type\":\"uint96\"}],\"name\":\"involuntaryAuthorizationDecrease\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"result\",\"type\":\"tuple\"}],\"name\":\"isDkgResultValid\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"},{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"isOperatorInPool\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"isOperatorUpToDate\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"walletMemberIndex\",\"type\":\"uint256\"}],\"name\":\"isWalletMember\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"}],\"name\":\"isWalletRegistered\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"joinSortitionPool\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"lifecycleOwner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"minimumAuthorization\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"\",\"type\":\"uint96\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"notifyDkgTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"},{\"internalType\":\"uint256[]\",\"name\":\"inactiveMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"bool\",\"name\":\"heartbeatFailed\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"}],\"internalType\":\"structFrostInactivity.Claim\",\"name\":\"claim\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"},{\"internalType\":\"uint32[]\",\"name\":\"groupMembers\",\"type\":\"uint32[]\"}],\"name\":\"notifyOperatorInactivity\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"notifySeedTimeout\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"operatorToStakingProvider\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"}],\"name\":\"pendingAuthorizationDecrease\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"\",\"type\":\"uint96\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"randomBeacon\",\"outputs\":[{\"internalType\":\"contractIRandomBeacon\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"registerOperator\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"registered\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"reimbursementPool\",\"outputs\":[{\"internalType\":\"contractReimbursementPool\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"}],\"name\":\"remainingAuthorizationDecreaseDelay\",\"outputs\":[{\"internalType\":\"uint64\",\"name\":\"\",\"type\":\"uint64\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"requestNewWallet\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"rewardParameters\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"maliciousDkgResultNotificationRewardMultiplier\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"sortitionPoolRewardsBanDuration\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint96\",\"name\":\"amount\",\"type\":\"uint96\"},{\"internalType\":\"uint256\",\"name\":\"rewardMultiplier\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"notifier\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"}],\"name\":\"seize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"selectGroup\",\"outputs\":[{\"internalType\":\"uint32[]\",\"name\":\"\",\"type\":\"uint32[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"slashingParameters\",\"outputs\":[{\"internalType\":\"uint96\",\"name\":\"maliciousDkgResultSlashingAmount\",\"type\":\"uint96\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"sortitionPool\",\"outputs\":[{\"internalType\":\"contractSortitionPool\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"}],\"name\":\"stakingProviderToOperator\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"dkgResult\",\"type\":\"tuple\"}],\"name\":\"submitDkgResult\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newGovernance\",\"type\":\"address\"}],\"name\":\"transferGovernance\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint96\",\"name\":\"_minimumAuthorization\",\"type\":\"uint96\"},{\"internalType\":\"uint64\",\"name\":\"_authorizationDecreaseDelay\",\"type\":\"uint64\"},{\"internalType\":\"uint64\",\"name\":\"_authorizationDecreaseChangePeriod\",\"type\":\"uint64\"}],\"name\":\"updateAuthorizationParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_seedTimeout\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_resultChallengePeriodLength\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_resultChallengeExtraGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_resultSubmissionTimeout\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_submitterPrecedencePeriodLength\",\"type\":\"uint256\"}],\"name\":\"updateDkgParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"dkgResultSubmissionGas\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"dkgResultApprovalGasOffset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"notifyOperatorInactivityGasOffset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"notifySeedTimeoutGasOffset\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"notifyDkgTimeoutNegativeGasOffset\",\"type\":\"uint256\"}],\"name\":\"updateGasParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_lifecycleOwner\",\"type\":\"address\"}],\"name\":\"updateLifecycleOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"operator\",\"type\":\"address\"}],\"name\":\"updateOperatorStatus\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractReimbursementPool\",\"name\":\"_reimbursementPool\",\"type\":\"address\"}],\"name\":\"updateReimbursementPool\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"maliciousDkgResultNotificationRewardMultiplier\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"sortitionPoolRewardsBanDuration\",\"type\":\"uint256\"}],\"name\":\"updateRewardParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint96\",\"name\":\"maliciousDkgResultSlashingAmount\",\"type\":\"uint96\"}],\"name\":\"updateSlashingParameters\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractIFrostWalletOwner\",\"name\":\"_walletOwner\",\"type\":\"address\"}],\"name\":\"updateWalletOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"contractIRandomBeacon\",\"name\":\"_randomBeacon\",\"type\":\"address\"}],\"name\":\"upgradeRandomBeacon\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"walletOwner\",\"outputs\":[{\"internalType\":\"contractIFrostWalletOwner\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"}],\"name\":\"withdrawIneligibleRewards\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"stakingProvider\",\"type\":\"address\"}],\"name\":\"withdrawRewards\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// FrostWalletRegistryABI is the input ABI used to generate the binding from. +// Deprecated: Use FrostWalletRegistryMetaData.ABI instead. +var FrostWalletRegistryABI = FrostWalletRegistryMetaData.ABI + +// FrostWalletRegistry is an auto generated Go binding around an Ethereum contract. +type FrostWalletRegistry struct { + FrostWalletRegistryCaller // Read-only binding to the contract + FrostWalletRegistryTransactor // Write-only binding to the contract + FrostWalletRegistryFilterer // Log filterer for contract events +} + +// FrostWalletRegistryCaller is an auto generated read-only Go binding around an Ethereum contract. +type FrostWalletRegistryCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FrostWalletRegistryTransactor is an auto generated write-only Go binding around an Ethereum contract. +type FrostWalletRegistryTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FrostWalletRegistryFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type FrostWalletRegistryFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FrostWalletRegistrySession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type FrostWalletRegistrySession struct { + Contract *FrostWalletRegistry // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FrostWalletRegistryCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type FrostWalletRegistryCallerSession struct { + Contract *FrostWalletRegistryCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// FrostWalletRegistryTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type FrostWalletRegistryTransactorSession struct { + Contract *FrostWalletRegistryTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FrostWalletRegistryRaw is an auto generated low-level Go binding around an Ethereum contract. +type FrostWalletRegistryRaw struct { + Contract *FrostWalletRegistry // Generic contract binding to access the raw methods on +} + +// FrostWalletRegistryCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type FrostWalletRegistryCallerRaw struct { + Contract *FrostWalletRegistryCaller // Generic read-only contract binding to access the raw methods on +} + +// FrostWalletRegistryTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type FrostWalletRegistryTransactorRaw struct { + Contract *FrostWalletRegistryTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewFrostWalletRegistry creates a new instance of FrostWalletRegistry, bound to a specific deployed contract. +func NewFrostWalletRegistry(address common.Address, backend bind.ContractBackend) (*FrostWalletRegistry, error) { + contract, err := bindFrostWalletRegistry(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &FrostWalletRegistry{FrostWalletRegistryCaller: FrostWalletRegistryCaller{contract: contract}, FrostWalletRegistryTransactor: FrostWalletRegistryTransactor{contract: contract}, FrostWalletRegistryFilterer: FrostWalletRegistryFilterer{contract: contract}}, nil +} + +// NewFrostWalletRegistryCaller creates a new read-only instance of FrostWalletRegistry, bound to a specific deployed contract. +func NewFrostWalletRegistryCaller(address common.Address, caller bind.ContractCaller) (*FrostWalletRegistryCaller, error) { + contract, err := bindFrostWalletRegistry(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &FrostWalletRegistryCaller{contract: contract}, nil +} + +// NewFrostWalletRegistryTransactor creates a new write-only instance of FrostWalletRegistry, bound to a specific deployed contract. +func NewFrostWalletRegistryTransactor(address common.Address, transactor bind.ContractTransactor) (*FrostWalletRegistryTransactor, error) { + contract, err := bindFrostWalletRegistry(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &FrostWalletRegistryTransactor{contract: contract}, nil +} + +// NewFrostWalletRegistryFilterer creates a new log filterer instance of FrostWalletRegistry, bound to a specific deployed contract. +func NewFrostWalletRegistryFilterer(address common.Address, filterer bind.ContractFilterer) (*FrostWalletRegistryFilterer, error) { + contract, err := bindFrostWalletRegistry(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &FrostWalletRegistryFilterer{contract: contract}, nil +} + +// bindFrostWalletRegistry binds a generic wrapper to an already deployed contract. +func bindFrostWalletRegistry(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := FrostWalletRegistryMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_FrostWalletRegistry *FrostWalletRegistryRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _FrostWalletRegistry.Contract.FrostWalletRegistryCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_FrostWalletRegistry *FrostWalletRegistryRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.FrostWalletRegistryTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_FrostWalletRegistry *FrostWalletRegistryRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.FrostWalletRegistryTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_FrostWalletRegistry *FrostWalletRegistryCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _FrostWalletRegistry.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_FrostWalletRegistry *FrostWalletRegistryTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_FrostWalletRegistry *FrostWalletRegistryTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.contract.Transact(opts, method, params...) +} + +// AuthorizationParameters is a free data retrieval call binding the contract method 0x7b14729e. +// +// Solidity: function authorizationParameters() view returns(uint96 minimumAuthorization, uint64 authorizationDecreaseDelay, uint64 authorizationDecreaseChangePeriod) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) AuthorizationParameters(opts *bind.CallOpts) (struct { + MinimumAuthorization *big.Int + AuthorizationDecreaseDelay uint64 + AuthorizationDecreaseChangePeriod uint64 +}, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "authorizationParameters") + + outstruct := new(struct { + MinimumAuthorization *big.Int + AuthorizationDecreaseDelay uint64 + AuthorizationDecreaseChangePeriod uint64 + }) + if err != nil { + return *outstruct, err + } + + outstruct.MinimumAuthorization = *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + outstruct.AuthorizationDecreaseDelay = *abi.ConvertType(out[1], new(uint64)).(*uint64) + outstruct.AuthorizationDecreaseChangePeriod = *abi.ConvertType(out[2], new(uint64)).(*uint64) + + return *outstruct, err + +} + +// AuthorizationParameters is a free data retrieval call binding the contract method 0x7b14729e. +// +// Solidity: function authorizationParameters() view returns(uint96 minimumAuthorization, uint64 authorizationDecreaseDelay, uint64 authorizationDecreaseChangePeriod) +func (_FrostWalletRegistry *FrostWalletRegistrySession) AuthorizationParameters() (struct { + MinimumAuthorization *big.Int + AuthorizationDecreaseDelay uint64 + AuthorizationDecreaseChangePeriod uint64 +}, error) { + return _FrostWalletRegistry.Contract.AuthorizationParameters(&_FrostWalletRegistry.CallOpts) +} + +// AuthorizationParameters is a free data retrieval call binding the contract method 0x7b14729e. +// +// Solidity: function authorizationParameters() view returns(uint96 minimumAuthorization, uint64 authorizationDecreaseDelay, uint64 authorizationDecreaseChangePeriod) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) AuthorizationParameters() (struct { + MinimumAuthorization *big.Int + AuthorizationDecreaseDelay uint64 + AuthorizationDecreaseChangePeriod uint64 +}, error) { + return _FrostWalletRegistry.Contract.AuthorizationParameters(&_FrostWalletRegistry.CallOpts) +} + +// AuthorizationSource is a free data retrieval call binding the contract method 0x0a3abae9. +// +// Solidity: function authorizationSource() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) AuthorizationSource(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "authorizationSource") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// AuthorizationSource is a free data retrieval call binding the contract method 0x0a3abae9. +// +// Solidity: function authorizationSource() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) AuthorizationSource() (common.Address, error) { + return _FrostWalletRegistry.Contract.AuthorizationSource(&_FrostWalletRegistry.CallOpts) +} + +// AuthorizationSource is a free data retrieval call binding the contract method 0x0a3abae9. +// +// Solidity: function authorizationSource() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) AuthorizationSource() (common.Address, error) { + return _FrostWalletRegistry.Contract.AuthorizationSource(&_FrostWalletRegistry.CallOpts) +} + +// AvailableRewards is a free data retrieval call binding the contract method 0xf854a27f. +// +// Solidity: function availableRewards(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) AvailableRewards(opts *bind.CallOpts, stakingProvider common.Address) (*big.Int, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "availableRewards", stakingProvider) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// AvailableRewards is a free data retrieval call binding the contract method 0xf854a27f. +// +// Solidity: function availableRewards(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistrySession) AvailableRewards(stakingProvider common.Address) (*big.Int, error) { + return _FrostWalletRegistry.Contract.AvailableRewards(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// AvailableRewards is a free data retrieval call binding the contract method 0xf854a27f. +// +// Solidity: function availableRewards(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) AvailableRewards(stakingProvider common.Address) (*big.Int, error) { + return _FrostWalletRegistry.Contract.AvailableRewards(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// DkgParameters is a free data retrieval call binding the contract method 0x08aa090b. +// +// Solidity: function dkgParameters() view returns((uint256,uint256,uint256,uint256,uint256)) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) DkgParameters(opts *bind.CallOpts) (FrostDkgParameters, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "dkgParameters") + + if err != nil { + return *new(FrostDkgParameters), err + } + + out0 := *abi.ConvertType(out[0], new(FrostDkgParameters)).(*FrostDkgParameters) + + return out0, err + +} + +// DkgParameters is a free data retrieval call binding the contract method 0x08aa090b. +// +// Solidity: function dkgParameters() view returns((uint256,uint256,uint256,uint256,uint256)) +func (_FrostWalletRegistry *FrostWalletRegistrySession) DkgParameters() (FrostDkgParameters, error) { + return _FrostWalletRegistry.Contract.DkgParameters(&_FrostWalletRegistry.CallOpts) +} + +// DkgParameters is a free data retrieval call binding the contract method 0x08aa090b. +// +// Solidity: function dkgParameters() view returns((uint256,uint256,uint256,uint256,uint256)) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) DkgParameters() (FrostDkgParameters, error) { + return _FrostWalletRegistry.Contract.DkgParameters(&_FrostWalletRegistry.CallOpts) +} + +// EligibleStake is a free data retrieval call binding the contract method 0x7e33cba6. +// +// Solidity: function eligibleStake(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) EligibleStake(opts *bind.CallOpts, stakingProvider common.Address) (*big.Int, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "eligibleStake", stakingProvider) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// EligibleStake is a free data retrieval call binding the contract method 0x7e33cba6. +// +// Solidity: function eligibleStake(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistrySession) EligibleStake(stakingProvider common.Address) (*big.Int, error) { + return _FrostWalletRegistry.Contract.EligibleStake(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// EligibleStake is a free data retrieval call binding the contract method 0x7e33cba6. +// +// Solidity: function eligibleStake(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) EligibleStake(stakingProvider common.Address) (*big.Int, error) { + return _FrostWalletRegistry.Contract.EligibleStake(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// GasParameters is a free data retrieval call binding the contract method 0x88a59590. +// +// Solidity: function gasParameters() view returns(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) GasParameters(opts *bind.CallOpts) (struct { + DkgResultSubmissionGas *big.Int + DkgResultApprovalGasOffset *big.Int + NotifyOperatorInactivityGasOffset *big.Int + NotifySeedTimeoutGasOffset *big.Int + NotifyDkgTimeoutNegativeGasOffset *big.Int +}, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "gasParameters") + + outstruct := new(struct { + DkgResultSubmissionGas *big.Int + DkgResultApprovalGasOffset *big.Int + NotifyOperatorInactivityGasOffset *big.Int + NotifySeedTimeoutGasOffset *big.Int + NotifyDkgTimeoutNegativeGasOffset *big.Int + }) + if err != nil { + return *outstruct, err + } + + outstruct.DkgResultSubmissionGas = *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + outstruct.DkgResultApprovalGasOffset = *abi.ConvertType(out[1], new(*big.Int)).(**big.Int) + outstruct.NotifyOperatorInactivityGasOffset = *abi.ConvertType(out[2], new(*big.Int)).(**big.Int) + outstruct.NotifySeedTimeoutGasOffset = *abi.ConvertType(out[3], new(*big.Int)).(**big.Int) + outstruct.NotifyDkgTimeoutNegativeGasOffset = *abi.ConvertType(out[4], new(*big.Int)).(**big.Int) + + return *outstruct, err + +} + +// GasParameters is a free data retrieval call binding the contract method 0x88a59590. +// +// Solidity: function gasParameters() view returns(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) +func (_FrostWalletRegistry *FrostWalletRegistrySession) GasParameters() (struct { + DkgResultSubmissionGas *big.Int + DkgResultApprovalGasOffset *big.Int + NotifyOperatorInactivityGasOffset *big.Int + NotifySeedTimeoutGasOffset *big.Int + NotifyDkgTimeoutNegativeGasOffset *big.Int +}, error) { + return _FrostWalletRegistry.Contract.GasParameters(&_FrostWalletRegistry.CallOpts) +} + +// GasParameters is a free data retrieval call binding the contract method 0x88a59590. +// +// Solidity: function gasParameters() view returns(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) GasParameters() (struct { + DkgResultSubmissionGas *big.Int + DkgResultApprovalGasOffset *big.Int + NotifyOperatorInactivityGasOffset *big.Int + NotifySeedTimeoutGasOffset *big.Int + NotifyDkgTimeoutNegativeGasOffset *big.Int +}, error) { + return _FrostWalletRegistry.Contract.GasParameters(&_FrostWalletRegistry.CallOpts) +} + +// GetWallet is a free data retrieval call binding the contract method 0x789d392a. +// +// Solidity: function getWallet(bytes32 walletID) view returns((bytes32,bytes32)) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) GetWallet(opts *bind.CallOpts, walletID [32]byte) (FrostRegistryWalletsWallet, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "getWallet", walletID) + + if err != nil { + return *new(FrostRegistryWalletsWallet), err + } + + out0 := *abi.ConvertType(out[0], new(FrostRegistryWalletsWallet)).(*FrostRegistryWalletsWallet) + + return out0, err + +} + +// GetWallet is a free data retrieval call binding the contract method 0x789d392a. +// +// Solidity: function getWallet(bytes32 walletID) view returns((bytes32,bytes32)) +func (_FrostWalletRegistry *FrostWalletRegistrySession) GetWallet(walletID [32]byte) (FrostRegistryWalletsWallet, error) { + return _FrostWalletRegistry.Contract.GetWallet(&_FrostWalletRegistry.CallOpts, walletID) +} + +// GetWallet is a free data retrieval call binding the contract method 0x789d392a. +// +// Solidity: function getWallet(bytes32 walletID) view returns((bytes32,bytes32)) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) GetWallet(walletID [32]byte) (FrostRegistryWalletsWallet, error) { + return _FrostWalletRegistry.Contract.GetWallet(&_FrostWalletRegistry.CallOpts, walletID) +} + +// GetWalletCreationState is a free data retrieval call binding the contract method 0xcc562388. +// +// Solidity: function getWalletCreationState() view returns(uint8) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) GetWalletCreationState(opts *bind.CallOpts) (uint8, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "getWalletCreationState") + + if err != nil { + return *new(uint8), err + } + + out0 := *abi.ConvertType(out[0], new(uint8)).(*uint8) + + return out0, err + +} + +// GetWalletCreationState is a free data retrieval call binding the contract method 0xcc562388. +// +// Solidity: function getWalletCreationState() view returns(uint8) +func (_FrostWalletRegistry *FrostWalletRegistrySession) GetWalletCreationState() (uint8, error) { + return _FrostWalletRegistry.Contract.GetWalletCreationState(&_FrostWalletRegistry.CallOpts) +} + +// GetWalletCreationState is a free data retrieval call binding the contract method 0xcc562388. +// +// Solidity: function getWalletCreationState() view returns(uint8) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) GetWalletCreationState() (uint8, error) { + return _FrostWalletRegistry.Contract.GetWalletCreationState(&_FrostWalletRegistry.CallOpts) +} + +// GetWalletXOnlyOutputKey is a free data retrieval call binding the contract method 0x13bd580a. +// +// Solidity: function getWalletXOnlyOutputKey(bytes32 walletID) view returns(bytes32) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) GetWalletXOnlyOutputKey(opts *bind.CallOpts, walletID [32]byte) ([32]byte, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "getWalletXOnlyOutputKey", walletID) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetWalletXOnlyOutputKey is a free data retrieval call binding the contract method 0x13bd580a. +// +// Solidity: function getWalletXOnlyOutputKey(bytes32 walletID) view returns(bytes32) +func (_FrostWalletRegistry *FrostWalletRegistrySession) GetWalletXOnlyOutputKey(walletID [32]byte) ([32]byte, error) { + return _FrostWalletRegistry.Contract.GetWalletXOnlyOutputKey(&_FrostWalletRegistry.CallOpts, walletID) +} + +// GetWalletXOnlyOutputKey is a free data retrieval call binding the contract method 0x13bd580a. +// +// Solidity: function getWalletXOnlyOutputKey(bytes32 walletID) view returns(bytes32) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) GetWalletXOnlyOutputKey(walletID [32]byte) ([32]byte, error) { + return _FrostWalletRegistry.Contract.GetWalletXOnlyOutputKey(&_FrostWalletRegistry.CallOpts, walletID) +} + +// Governance is a free data retrieval call binding the contract method 0x5aa6e675. +// +// Solidity: function governance() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) Governance(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "governance") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Governance is a free data retrieval call binding the contract method 0x5aa6e675. +// +// Solidity: function governance() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) Governance() (common.Address, error) { + return _FrostWalletRegistry.Contract.Governance(&_FrostWalletRegistry.CallOpts) +} + +// Governance is a free data retrieval call binding the contract method 0x5aa6e675. +// +// Solidity: function governance() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) Governance() (common.Address, error) { + return _FrostWalletRegistry.Contract.Governance(&_FrostWalletRegistry.CallOpts) +} + +// HasDkgTimedOut is a free data retrieval call binding the contract method 0x68c34948. +// +// Solidity: function hasDkgTimedOut() view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) HasDkgTimedOut(opts *bind.CallOpts) (bool, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "hasDkgTimedOut") + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// HasDkgTimedOut is a free data retrieval call binding the contract method 0x68c34948. +// +// Solidity: function hasDkgTimedOut() view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistrySession) HasDkgTimedOut() (bool, error) { + return _FrostWalletRegistry.Contract.HasDkgTimedOut(&_FrostWalletRegistry.CallOpts) +} + +// HasDkgTimedOut is a free data retrieval call binding the contract method 0x68c34948. +// +// Solidity: function hasDkgTimedOut() view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) HasDkgTimedOut() (bool, error) { + return _FrostWalletRegistry.Contract.HasDkgTimedOut(&_FrostWalletRegistry.CallOpts) +} + +// HasSeedTimedOut is a free data retrieval call binding the contract method 0x770124d3. +// +// Solidity: function hasSeedTimedOut() view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) HasSeedTimedOut(opts *bind.CallOpts) (bool, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "hasSeedTimedOut") + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// HasSeedTimedOut is a free data retrieval call binding the contract method 0x770124d3. +// +// Solidity: function hasSeedTimedOut() view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistrySession) HasSeedTimedOut() (bool, error) { + return _FrostWalletRegistry.Contract.HasSeedTimedOut(&_FrostWalletRegistry.CallOpts) +} + +// HasSeedTimedOut is a free data retrieval call binding the contract method 0x770124d3. +// +// Solidity: function hasSeedTimedOut() view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) HasSeedTimedOut() (bool, error) { + return _FrostWalletRegistry.Contract.HasSeedTimedOut(&_FrostWalletRegistry.CallOpts) +} + +// InactivityClaimNonce is a free data retrieval call binding the contract method 0x830f9e02. +// +// Solidity: function inactivityClaimNonce(bytes32 ) view returns(uint256) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) InactivityClaimNonce(opts *bind.CallOpts, arg0 [32]byte) (*big.Int, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "inactivityClaimNonce", arg0) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// InactivityClaimNonce is a free data retrieval call binding the contract method 0x830f9e02. +// +// Solidity: function inactivityClaimNonce(bytes32 ) view returns(uint256) +func (_FrostWalletRegistry *FrostWalletRegistrySession) InactivityClaimNonce(arg0 [32]byte) (*big.Int, error) { + return _FrostWalletRegistry.Contract.InactivityClaimNonce(&_FrostWalletRegistry.CallOpts, arg0) +} + +// InactivityClaimNonce is a free data retrieval call binding the contract method 0x830f9e02. +// +// Solidity: function inactivityClaimNonce(bytes32 ) view returns(uint256) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) InactivityClaimNonce(arg0 [32]byte) (*big.Int, error) { + return _FrostWalletRegistry.Contract.InactivityClaimNonce(&_FrostWalletRegistry.CallOpts, arg0) +} + +// IsDkgResultValid is a free data retrieval call binding the contract method 0x3b74e062. +// +// Solidity: function isDkgResultValid((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) view returns(bool, string) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) IsDkgResultValid(opts *bind.CallOpts, result FrostDkgResult) (bool, string, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "isDkgResultValid", result) + + if err != nil { + return *new(bool), *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + out1 := *abi.ConvertType(out[1], new(string)).(*string) + + return out0, out1, err + +} + +// IsDkgResultValid is a free data retrieval call binding the contract method 0x3b74e062. +// +// Solidity: function isDkgResultValid((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) view returns(bool, string) +func (_FrostWalletRegistry *FrostWalletRegistrySession) IsDkgResultValid(result FrostDkgResult) (bool, string, error) { + return _FrostWalletRegistry.Contract.IsDkgResultValid(&_FrostWalletRegistry.CallOpts, result) +} + +// IsDkgResultValid is a free data retrieval call binding the contract method 0x3b74e062. +// +// Solidity: function isDkgResultValid((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) view returns(bool, string) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) IsDkgResultValid(result FrostDkgResult) (bool, string, error) { + return _FrostWalletRegistry.Contract.IsDkgResultValid(&_FrostWalletRegistry.CallOpts, result) +} + +// IsOperatorInPool is a free data retrieval call binding the contract method 0xf7186ce0. +// +// Solidity: function isOperatorInPool(address operator) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) IsOperatorInPool(opts *bind.CallOpts, operator common.Address) (bool, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "isOperatorInPool", operator) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsOperatorInPool is a free data retrieval call binding the contract method 0xf7186ce0. +// +// Solidity: function isOperatorInPool(address operator) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistrySession) IsOperatorInPool(operator common.Address) (bool, error) { + return _FrostWalletRegistry.Contract.IsOperatorInPool(&_FrostWalletRegistry.CallOpts, operator) +} + +// IsOperatorInPool is a free data retrieval call binding the contract method 0xf7186ce0. +// +// Solidity: function isOperatorInPool(address operator) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) IsOperatorInPool(operator common.Address) (bool, error) { + return _FrostWalletRegistry.Contract.IsOperatorInPool(&_FrostWalletRegistry.CallOpts, operator) +} + +// IsOperatorUpToDate is a free data retrieval call binding the contract method 0xe686440f. +// +// Solidity: function isOperatorUpToDate(address operator) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) IsOperatorUpToDate(opts *bind.CallOpts, operator common.Address) (bool, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "isOperatorUpToDate", operator) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsOperatorUpToDate is a free data retrieval call binding the contract method 0xe686440f. +// +// Solidity: function isOperatorUpToDate(address operator) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistrySession) IsOperatorUpToDate(operator common.Address) (bool, error) { + return _FrostWalletRegistry.Contract.IsOperatorUpToDate(&_FrostWalletRegistry.CallOpts, operator) +} + +// IsOperatorUpToDate is a free data retrieval call binding the contract method 0xe686440f. +// +// Solidity: function isOperatorUpToDate(address operator) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) IsOperatorUpToDate(operator common.Address) (bool, error) { + return _FrostWalletRegistry.Contract.IsOperatorUpToDate(&_FrostWalletRegistry.CallOpts, operator) +} + +// IsWalletMember is a free data retrieval call binding the contract method 0xdf07ce59. +// +// Solidity: function isWalletMember(bytes32 walletID, uint32[] walletMembersIDs, address operator, uint256 walletMemberIndex) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) IsWalletMember(opts *bind.CallOpts, walletID [32]byte, walletMembersIDs []uint32, operator common.Address, walletMemberIndex *big.Int) (bool, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "isWalletMember", walletID, walletMembersIDs, operator, walletMemberIndex) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsWalletMember is a free data retrieval call binding the contract method 0xdf07ce59. +// +// Solidity: function isWalletMember(bytes32 walletID, uint32[] walletMembersIDs, address operator, uint256 walletMemberIndex) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistrySession) IsWalletMember(walletID [32]byte, walletMembersIDs []uint32, operator common.Address, walletMemberIndex *big.Int) (bool, error) { + return _FrostWalletRegistry.Contract.IsWalletMember(&_FrostWalletRegistry.CallOpts, walletID, walletMembersIDs, operator, walletMemberIndex) +} + +// IsWalletMember is a free data retrieval call binding the contract method 0xdf07ce59. +// +// Solidity: function isWalletMember(bytes32 walletID, uint32[] walletMembersIDs, address operator, uint256 walletMemberIndex) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) IsWalletMember(walletID [32]byte, walletMembersIDs []uint32, operator common.Address, walletMemberIndex *big.Int) (bool, error) { + return _FrostWalletRegistry.Contract.IsWalletMember(&_FrostWalletRegistry.CallOpts, walletID, walletMembersIDs, operator, walletMemberIndex) +} + +// IsWalletRegistered is a free data retrieval call binding the contract method 0x4d99f473. +// +// Solidity: function isWalletRegistered(bytes32 walletID) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) IsWalletRegistered(opts *bind.CallOpts, walletID [32]byte) (bool, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "isWalletRegistered", walletID) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsWalletRegistered is a free data retrieval call binding the contract method 0x4d99f473. +// +// Solidity: function isWalletRegistered(bytes32 walletID) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistrySession) IsWalletRegistered(walletID [32]byte) (bool, error) { + return _FrostWalletRegistry.Contract.IsWalletRegistered(&_FrostWalletRegistry.CallOpts, walletID) +} + +// IsWalletRegistered is a free data retrieval call binding the contract method 0x4d99f473. +// +// Solidity: function isWalletRegistered(bytes32 walletID) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) IsWalletRegistered(walletID [32]byte) (bool, error) { + return _FrostWalletRegistry.Contract.IsWalletRegistered(&_FrostWalletRegistry.CallOpts, walletID) +} + +// LifecycleOwner is a free data retrieval call binding the contract method 0x7780dea1. +// +// Solidity: function lifecycleOwner() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) LifecycleOwner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "lifecycleOwner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// LifecycleOwner is a free data retrieval call binding the contract method 0x7780dea1. +// +// Solidity: function lifecycleOwner() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) LifecycleOwner() (common.Address, error) { + return _FrostWalletRegistry.Contract.LifecycleOwner(&_FrostWalletRegistry.CallOpts) +} + +// LifecycleOwner is a free data retrieval call binding the contract method 0x7780dea1. +// +// Solidity: function lifecycleOwner() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) LifecycleOwner() (common.Address, error) { + return _FrostWalletRegistry.Contract.LifecycleOwner(&_FrostWalletRegistry.CallOpts) +} + +// MinimumAuthorization is a free data retrieval call binding the contract method 0xf0820c92. +// +// Solidity: function minimumAuthorization() view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) MinimumAuthorization(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "minimumAuthorization") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// MinimumAuthorization is a free data retrieval call binding the contract method 0xf0820c92. +// +// Solidity: function minimumAuthorization() view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistrySession) MinimumAuthorization() (*big.Int, error) { + return _FrostWalletRegistry.Contract.MinimumAuthorization(&_FrostWalletRegistry.CallOpts) +} + +// MinimumAuthorization is a free data retrieval call binding the contract method 0xf0820c92. +// +// Solidity: function minimumAuthorization() view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) MinimumAuthorization() (*big.Int, error) { + return _FrostWalletRegistry.Contract.MinimumAuthorization(&_FrostWalletRegistry.CallOpts) +} + +// OperatorToStakingProvider is a free data retrieval call binding the contract method 0xded56d45. +// +// Solidity: function operatorToStakingProvider(address operator) view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) OperatorToStakingProvider(opts *bind.CallOpts, operator common.Address) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "operatorToStakingProvider", operator) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// OperatorToStakingProvider is a free data retrieval call binding the contract method 0xded56d45. +// +// Solidity: function operatorToStakingProvider(address operator) view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) OperatorToStakingProvider(operator common.Address) (common.Address, error) { + return _FrostWalletRegistry.Contract.OperatorToStakingProvider(&_FrostWalletRegistry.CallOpts, operator) +} + +// OperatorToStakingProvider is a free data retrieval call binding the contract method 0xded56d45. +// +// Solidity: function operatorToStakingProvider(address operator) view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) OperatorToStakingProvider(operator common.Address) (common.Address, error) { + return _FrostWalletRegistry.Contract.OperatorToStakingProvider(&_FrostWalletRegistry.CallOpts, operator) +} + +// PendingAuthorizationDecrease is a free data retrieval call binding the contract method 0xfd2a4788. +// +// Solidity: function pendingAuthorizationDecrease(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) PendingAuthorizationDecrease(opts *bind.CallOpts, stakingProvider common.Address) (*big.Int, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "pendingAuthorizationDecrease", stakingProvider) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// PendingAuthorizationDecrease is a free data retrieval call binding the contract method 0xfd2a4788. +// +// Solidity: function pendingAuthorizationDecrease(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistrySession) PendingAuthorizationDecrease(stakingProvider common.Address) (*big.Int, error) { + return _FrostWalletRegistry.Contract.PendingAuthorizationDecrease(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// PendingAuthorizationDecrease is a free data retrieval call binding the contract method 0xfd2a4788. +// +// Solidity: function pendingAuthorizationDecrease(address stakingProvider) view returns(uint96) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) PendingAuthorizationDecrease(stakingProvider common.Address) (*big.Int, error) { + return _FrostWalletRegistry.Contract.PendingAuthorizationDecrease(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// RandomBeacon is a free data retrieval call binding the contract method 0x153622b3. +// +// Solidity: function randomBeacon() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) RandomBeacon(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "randomBeacon") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// RandomBeacon is a free data retrieval call binding the contract method 0x153622b3. +// +// Solidity: function randomBeacon() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) RandomBeacon() (common.Address, error) { + return _FrostWalletRegistry.Contract.RandomBeacon(&_FrostWalletRegistry.CallOpts) +} + +// RandomBeacon is a free data retrieval call binding the contract method 0x153622b3. +// +// Solidity: function randomBeacon() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) RandomBeacon() (common.Address, error) { + return _FrostWalletRegistry.Contract.RandomBeacon(&_FrostWalletRegistry.CallOpts) +} + +// Registered is a free data retrieval call binding the contract method 0x5524d548. +// +// Solidity: function registered(bytes32 ) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) Registered(opts *bind.CallOpts, arg0 [32]byte) (bool, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "registered", arg0) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// Registered is a free data retrieval call binding the contract method 0x5524d548. +// +// Solidity: function registered(bytes32 ) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistrySession) Registered(arg0 [32]byte) (bool, error) { + return _FrostWalletRegistry.Contract.Registered(&_FrostWalletRegistry.CallOpts, arg0) +} + +// Registered is a free data retrieval call binding the contract method 0x5524d548. +// +// Solidity: function registered(bytes32 ) view returns(bool) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) Registered(arg0 [32]byte) (bool, error) { + return _FrostWalletRegistry.Contract.Registered(&_FrostWalletRegistry.CallOpts, arg0) +} + +// ReimbursementPool is a free data retrieval call binding the contract method 0xc09975cd. +// +// Solidity: function reimbursementPool() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) ReimbursementPool(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "reimbursementPool") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// ReimbursementPool is a free data retrieval call binding the contract method 0xc09975cd. +// +// Solidity: function reimbursementPool() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) ReimbursementPool() (common.Address, error) { + return _FrostWalletRegistry.Contract.ReimbursementPool(&_FrostWalletRegistry.CallOpts) +} + +// ReimbursementPool is a free data retrieval call binding the contract method 0xc09975cd. +// +// Solidity: function reimbursementPool() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) ReimbursementPool() (common.Address, error) { + return _FrostWalletRegistry.Contract.ReimbursementPool(&_FrostWalletRegistry.CallOpts) +} + +// RemainingAuthorizationDecreaseDelay is a free data retrieval call binding the contract method 0x9c9de028. +// +// Solidity: function remainingAuthorizationDecreaseDelay(address stakingProvider) view returns(uint64) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) RemainingAuthorizationDecreaseDelay(opts *bind.CallOpts, stakingProvider common.Address) (uint64, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "remainingAuthorizationDecreaseDelay", stakingProvider) + + if err != nil { + return *new(uint64), err + } + + out0 := *abi.ConvertType(out[0], new(uint64)).(*uint64) + + return out0, err + +} + +// RemainingAuthorizationDecreaseDelay is a free data retrieval call binding the contract method 0x9c9de028. +// +// Solidity: function remainingAuthorizationDecreaseDelay(address stakingProvider) view returns(uint64) +func (_FrostWalletRegistry *FrostWalletRegistrySession) RemainingAuthorizationDecreaseDelay(stakingProvider common.Address) (uint64, error) { + return _FrostWalletRegistry.Contract.RemainingAuthorizationDecreaseDelay(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// RemainingAuthorizationDecreaseDelay is a free data retrieval call binding the contract method 0x9c9de028. +// +// Solidity: function remainingAuthorizationDecreaseDelay(address stakingProvider) view returns(uint64) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) RemainingAuthorizationDecreaseDelay(stakingProvider common.Address) (uint64, error) { + return _FrostWalletRegistry.Contract.RemainingAuthorizationDecreaseDelay(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// RewardParameters is a free data retrieval call binding the contract method 0x52902301. +// +// Solidity: function rewardParameters() view returns(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) RewardParameters(opts *bind.CallOpts) (struct { + MaliciousDkgResultNotificationRewardMultiplier *big.Int + SortitionPoolRewardsBanDuration *big.Int +}, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "rewardParameters") + + outstruct := new(struct { + MaliciousDkgResultNotificationRewardMultiplier *big.Int + SortitionPoolRewardsBanDuration *big.Int + }) + if err != nil { + return *outstruct, err + } + + outstruct.MaliciousDkgResultNotificationRewardMultiplier = *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + outstruct.SortitionPoolRewardsBanDuration = *abi.ConvertType(out[1], new(*big.Int)).(**big.Int) + + return *outstruct, err + +} + +// RewardParameters is a free data retrieval call binding the contract method 0x52902301. +// +// Solidity: function rewardParameters() view returns(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) +func (_FrostWalletRegistry *FrostWalletRegistrySession) RewardParameters() (struct { + MaliciousDkgResultNotificationRewardMultiplier *big.Int + SortitionPoolRewardsBanDuration *big.Int +}, error) { + return _FrostWalletRegistry.Contract.RewardParameters(&_FrostWalletRegistry.CallOpts) +} + +// RewardParameters is a free data retrieval call binding the contract method 0x52902301. +// +// Solidity: function rewardParameters() view returns(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) RewardParameters() (struct { + MaliciousDkgResultNotificationRewardMultiplier *big.Int + SortitionPoolRewardsBanDuration *big.Int +}, error) { + return _FrostWalletRegistry.Contract.RewardParameters(&_FrostWalletRegistry.CallOpts) +} + +// SelectGroup is a free data retrieval call binding the contract method 0xe03e4535. +// +// Solidity: function selectGroup() view returns(uint32[]) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) SelectGroup(opts *bind.CallOpts) ([]uint32, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "selectGroup") + + if err != nil { + return *new([]uint32), err + } + + out0 := *abi.ConvertType(out[0], new([]uint32)).(*[]uint32) + + return out0, err + +} + +// SelectGroup is a free data retrieval call binding the contract method 0xe03e4535. +// +// Solidity: function selectGroup() view returns(uint32[]) +func (_FrostWalletRegistry *FrostWalletRegistrySession) SelectGroup() ([]uint32, error) { + return _FrostWalletRegistry.Contract.SelectGroup(&_FrostWalletRegistry.CallOpts) +} + +// SelectGroup is a free data retrieval call binding the contract method 0xe03e4535. +// +// Solidity: function selectGroup() view returns(uint32[]) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) SelectGroup() ([]uint32, error) { + return _FrostWalletRegistry.Contract.SelectGroup(&_FrostWalletRegistry.CallOpts) +} + +// SlashingParameters is a free data retrieval call binding the contract method 0x1d35fa63. +// +// Solidity: function slashingParameters() view returns(uint96 maliciousDkgResultSlashingAmount) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) SlashingParameters(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "slashingParameters") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// SlashingParameters is a free data retrieval call binding the contract method 0x1d35fa63. +// +// Solidity: function slashingParameters() view returns(uint96 maliciousDkgResultSlashingAmount) +func (_FrostWalletRegistry *FrostWalletRegistrySession) SlashingParameters() (*big.Int, error) { + return _FrostWalletRegistry.Contract.SlashingParameters(&_FrostWalletRegistry.CallOpts) +} + +// SlashingParameters is a free data retrieval call binding the contract method 0x1d35fa63. +// +// Solidity: function slashingParameters() view returns(uint96 maliciousDkgResultSlashingAmount) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) SlashingParameters() (*big.Int, error) { + return _FrostWalletRegistry.Contract.SlashingParameters(&_FrostWalletRegistry.CallOpts) +} + +// SortitionPool is a free data retrieval call binding the contract method 0xb54a2374. +// +// Solidity: function sortitionPool() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) SortitionPool(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "sortitionPool") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// SortitionPool is a free data retrieval call binding the contract method 0xb54a2374. +// +// Solidity: function sortitionPool() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) SortitionPool() (common.Address, error) { + return _FrostWalletRegistry.Contract.SortitionPool(&_FrostWalletRegistry.CallOpts) +} + +// SortitionPool is a free data retrieval call binding the contract method 0xb54a2374. +// +// Solidity: function sortitionPool() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) SortitionPool() (common.Address, error) { + return _FrostWalletRegistry.Contract.SortitionPool(&_FrostWalletRegistry.CallOpts) +} + +// StakingProviderToOperator is a free data retrieval call binding the contract method 0xc7c49c98. +// +// Solidity: function stakingProviderToOperator(address stakingProvider) view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) StakingProviderToOperator(opts *bind.CallOpts, stakingProvider common.Address) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "stakingProviderToOperator", stakingProvider) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// StakingProviderToOperator is a free data retrieval call binding the contract method 0xc7c49c98. +// +// Solidity: function stakingProviderToOperator(address stakingProvider) view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) StakingProviderToOperator(stakingProvider common.Address) (common.Address, error) { + return _FrostWalletRegistry.Contract.StakingProviderToOperator(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// StakingProviderToOperator is a free data retrieval call binding the contract method 0xc7c49c98. +// +// Solidity: function stakingProviderToOperator(address stakingProvider) view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) StakingProviderToOperator(stakingProvider common.Address) (common.Address, error) { + return _FrostWalletRegistry.Contract.StakingProviderToOperator(&_FrostWalletRegistry.CallOpts, stakingProvider) +} + +// WalletOwner is a free data retrieval call binding the contract method 0x1ae879e8. +// +// Solidity: function walletOwner() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) WalletOwner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "walletOwner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// WalletOwner is a free data retrieval call binding the contract method 0x1ae879e8. +// +// Solidity: function walletOwner() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistrySession) WalletOwner() (common.Address, error) { + return _FrostWalletRegistry.Contract.WalletOwner(&_FrostWalletRegistry.CallOpts) +} + +// WalletOwner is a free data retrieval call binding the contract method 0x1ae879e8. +// +// Solidity: function walletOwner() view returns(address) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) WalletOwner() (common.Address, error) { + return _FrostWalletRegistry.Contract.WalletOwner(&_FrostWalletRegistry.CallOpts) +} + +// BeaconCallback is a paid mutator transaction binding the contract method 0x6febd464. +// +// Solidity: function __beaconCallback(uint256 relayEntry, uint256 ) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) BeaconCallback(opts *bind.TransactOpts, relayEntry *big.Int, arg1 *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "__beaconCallback", relayEntry, arg1) +} + +// BeaconCallback is a paid mutator transaction binding the contract method 0x6febd464. +// +// Solidity: function __beaconCallback(uint256 relayEntry, uint256 ) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) BeaconCallback(relayEntry *big.Int, arg1 *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.BeaconCallback(&_FrostWalletRegistry.TransactOpts, relayEntry, arg1) +} + +// BeaconCallback is a paid mutator transaction binding the contract method 0x6febd464. +// +// Solidity: function __beaconCallback(uint256 relayEntry, uint256 ) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) BeaconCallback(relayEntry *big.Int, arg1 *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.BeaconCallback(&_FrostWalletRegistry.TransactOpts, relayEntry, arg1) +} + +// ApproveAuthorizationDecrease is a paid mutator transaction binding the contract method 0x75e0ae5a. +// +// Solidity: function approveAuthorizationDecrease(address stakingProvider) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) ApproveAuthorizationDecrease(opts *bind.TransactOpts, stakingProvider common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "approveAuthorizationDecrease", stakingProvider) +} + +// ApproveAuthorizationDecrease is a paid mutator transaction binding the contract method 0x75e0ae5a. +// +// Solidity: function approveAuthorizationDecrease(address stakingProvider) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) ApproveAuthorizationDecrease(stakingProvider common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ApproveAuthorizationDecrease(&_FrostWalletRegistry.TransactOpts, stakingProvider) +} + +// ApproveAuthorizationDecrease is a paid mutator transaction binding the contract method 0x75e0ae5a. +// +// Solidity: function approveAuthorizationDecrease(address stakingProvider) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) ApproveAuthorizationDecrease(stakingProvider common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ApproveAuthorizationDecrease(&_FrostWalletRegistry.TransactOpts, stakingProvider) +} + +// ApproveDkgResult is a paid mutator transaction binding the contract method 0xcf2feddd. +// +// Solidity: function approveDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) ApproveDkgResult(opts *bind.TransactOpts, dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "approveDkgResult", dkgResult) +} + +// ApproveDkgResult is a paid mutator transaction binding the contract method 0xcf2feddd. +// +// Solidity: function approveDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) ApproveDkgResult(dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ApproveDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// ApproveDkgResult is a paid mutator transaction binding the contract method 0xcf2feddd. +// +// Solidity: function approveDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) ApproveDkgResult(dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ApproveDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// AuthorizationDecreaseRequested is a paid mutator transaction binding the contract method 0x6a7f7a90. +// +// Solidity: function authorizationDecreaseRequested(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) AuthorizationDecreaseRequested(opts *bind.TransactOpts, stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "authorizationDecreaseRequested", stakingProvider, fromAmount, toAmount) +} + +// AuthorizationDecreaseRequested is a paid mutator transaction binding the contract method 0x6a7f7a90. +// +// Solidity: function authorizationDecreaseRequested(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) AuthorizationDecreaseRequested(stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.AuthorizationDecreaseRequested(&_FrostWalletRegistry.TransactOpts, stakingProvider, fromAmount, toAmount) +} + +// AuthorizationDecreaseRequested is a paid mutator transaction binding the contract method 0x6a7f7a90. +// +// Solidity: function authorizationDecreaseRequested(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) AuthorizationDecreaseRequested(stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.AuthorizationDecreaseRequested(&_FrostWalletRegistry.TransactOpts, stakingProvider, fromAmount, toAmount) +} + +// AuthorizationIncreased is a paid mutator transaction binding the contract method 0xc9bacaad. +// +// Solidity: function authorizationIncreased(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) AuthorizationIncreased(opts *bind.TransactOpts, stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "authorizationIncreased", stakingProvider, fromAmount, toAmount) +} + +// AuthorizationIncreased is a paid mutator transaction binding the contract method 0xc9bacaad. +// +// Solidity: function authorizationIncreased(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) AuthorizationIncreased(stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.AuthorizationIncreased(&_FrostWalletRegistry.TransactOpts, stakingProvider, fromAmount, toAmount) +} + +// AuthorizationIncreased is a paid mutator transaction binding the contract method 0xc9bacaad. +// +// Solidity: function authorizationIncreased(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) AuthorizationIncreased(stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.AuthorizationIncreased(&_FrostWalletRegistry.TransactOpts, stakingProvider, fromAmount, toAmount) +} + +// ChallengeDkgResult is a paid mutator transaction binding the contract method 0x24ac833e. +// +// Solidity: function challengeDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) ChallengeDkgResult(opts *bind.TransactOpts, dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "challengeDkgResult", dkgResult) +} + +// ChallengeDkgResult is a paid mutator transaction binding the contract method 0x24ac833e. +// +// Solidity: function challengeDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) ChallengeDkgResult(dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ChallengeDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// ChallengeDkgResult is a paid mutator transaction binding the contract method 0x24ac833e. +// +// Solidity: function challengeDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) ChallengeDkgResult(dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ChallengeDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// CloseWallet is a paid mutator transaction binding the contract method 0x343bb927. +// +// Solidity: function closeWallet(bytes32 walletID) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) CloseWallet(opts *bind.TransactOpts, walletID [32]byte) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "closeWallet", walletID) +} + +// CloseWallet is a paid mutator transaction binding the contract method 0x343bb927. +// +// Solidity: function closeWallet(bytes32 walletID) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) CloseWallet(walletID [32]byte) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.CloseWallet(&_FrostWalletRegistry.TransactOpts, walletID) +} + +// CloseWallet is a paid mutator transaction binding the contract method 0x343bb927. +// +// Solidity: function closeWallet(bytes32 walletID) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) CloseWallet(walletID [32]byte) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.CloseWallet(&_FrostWalletRegistry.TransactOpts, walletID) +} + +// Initialize is a paid mutator transaction binding the contract method 0xf8c8765e. +// +// Solidity: function initialize(address _ecdsaDkgValidator, address _randomBeacon, address _reimbursementPool, address _bridge) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) Initialize(opts *bind.TransactOpts, _ecdsaDkgValidator common.Address, _randomBeacon common.Address, _reimbursementPool common.Address, _bridge common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "initialize", _ecdsaDkgValidator, _randomBeacon, _reimbursementPool, _bridge) +} + +// Initialize is a paid mutator transaction binding the contract method 0xf8c8765e. +// +// Solidity: function initialize(address _ecdsaDkgValidator, address _randomBeacon, address _reimbursementPool, address _bridge) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) Initialize(_ecdsaDkgValidator common.Address, _randomBeacon common.Address, _reimbursementPool common.Address, _bridge common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.Initialize(&_FrostWalletRegistry.TransactOpts, _ecdsaDkgValidator, _randomBeacon, _reimbursementPool, _bridge) +} + +// Initialize is a paid mutator transaction binding the contract method 0xf8c8765e. +// +// Solidity: function initialize(address _ecdsaDkgValidator, address _randomBeacon, address _reimbursementPool, address _bridge) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) Initialize(_ecdsaDkgValidator common.Address, _randomBeacon common.Address, _reimbursementPool common.Address, _bridge common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.Initialize(&_FrostWalletRegistry.TransactOpts, _ecdsaDkgValidator, _randomBeacon, _reimbursementPool, _bridge) +} + +// InitializeV2 is a paid mutator transaction binding the contract method 0x29b6eca9. +// +// Solidity: function initializeV2(address _authorizationSource) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) InitializeV2(opts *bind.TransactOpts, _authorizationSource common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "initializeV2", _authorizationSource) +} + +// InitializeV2 is a paid mutator transaction binding the contract method 0x29b6eca9. +// +// Solidity: function initializeV2(address _authorizationSource) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) InitializeV2(_authorizationSource common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.InitializeV2(&_FrostWalletRegistry.TransactOpts, _authorizationSource) +} + +// InitializeV2 is a paid mutator transaction binding the contract method 0x29b6eca9. +// +// Solidity: function initializeV2(address _authorizationSource) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) InitializeV2(_authorizationSource common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.InitializeV2(&_FrostWalletRegistry.TransactOpts, _authorizationSource) +} + +// InvoluntaryAuthorizationDecrease is a paid mutator transaction binding the contract method 0x14a85474. +// +// Solidity: function involuntaryAuthorizationDecrease(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) InvoluntaryAuthorizationDecrease(opts *bind.TransactOpts, stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "involuntaryAuthorizationDecrease", stakingProvider, fromAmount, toAmount) +} + +// InvoluntaryAuthorizationDecrease is a paid mutator transaction binding the contract method 0x14a85474. +// +// Solidity: function involuntaryAuthorizationDecrease(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) InvoluntaryAuthorizationDecrease(stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.InvoluntaryAuthorizationDecrease(&_FrostWalletRegistry.TransactOpts, stakingProvider, fromAmount, toAmount) +} + +// InvoluntaryAuthorizationDecrease is a paid mutator transaction binding the contract method 0x14a85474. +// +// Solidity: function involuntaryAuthorizationDecrease(address stakingProvider, uint96 fromAmount, uint96 toAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) InvoluntaryAuthorizationDecrease(stakingProvider common.Address, fromAmount *big.Int, toAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.InvoluntaryAuthorizationDecrease(&_FrostWalletRegistry.TransactOpts, stakingProvider, fromAmount, toAmount) +} + +// JoinSortitionPool is a paid mutator transaction binding the contract method 0x167f0517. +// +// Solidity: function joinSortitionPool() returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) JoinSortitionPool(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "joinSortitionPool") +} + +// JoinSortitionPool is a paid mutator transaction binding the contract method 0x167f0517. +// +// Solidity: function joinSortitionPool() returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) JoinSortitionPool() (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.JoinSortitionPool(&_FrostWalletRegistry.TransactOpts) +} + +// JoinSortitionPool is a paid mutator transaction binding the contract method 0x167f0517. +// +// Solidity: function joinSortitionPool() returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) JoinSortitionPool() (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.JoinSortitionPool(&_FrostWalletRegistry.TransactOpts) +} + +// NotifyDkgTimeout is a paid mutator transaction binding the contract method 0xd855c631. +// +// Solidity: function notifyDkgTimeout() returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) NotifyDkgTimeout(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "notifyDkgTimeout") +} + +// NotifyDkgTimeout is a paid mutator transaction binding the contract method 0xd855c631. +// +// Solidity: function notifyDkgTimeout() returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) NotifyDkgTimeout() (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.NotifyDkgTimeout(&_FrostWalletRegistry.TransactOpts) +} + +// NotifyDkgTimeout is a paid mutator transaction binding the contract method 0xd855c631. +// +// Solidity: function notifyDkgTimeout() returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) NotifyDkgTimeout() (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.NotifyDkgTimeout(&_FrostWalletRegistry.TransactOpts) +} + +// NotifyOperatorInactivity is a paid mutator transaction binding the contract method 0x9879d19b. +// +// Solidity: function notifyOperatorInactivity((bytes32,uint256[],bool,bytes,uint256[]) claim, uint256 nonce, uint32[] groupMembers) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) NotifyOperatorInactivity(opts *bind.TransactOpts, claim FrostInactivityClaim, nonce *big.Int, groupMembers []uint32) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "notifyOperatorInactivity", claim, nonce, groupMembers) +} + +// NotifyOperatorInactivity is a paid mutator transaction binding the contract method 0x9879d19b. +// +// Solidity: function notifyOperatorInactivity((bytes32,uint256[],bool,bytes,uint256[]) claim, uint256 nonce, uint32[] groupMembers) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) NotifyOperatorInactivity(claim FrostInactivityClaim, nonce *big.Int, groupMembers []uint32) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.NotifyOperatorInactivity(&_FrostWalletRegistry.TransactOpts, claim, nonce, groupMembers) +} + +// NotifyOperatorInactivity is a paid mutator transaction binding the contract method 0x9879d19b. +// +// Solidity: function notifyOperatorInactivity((bytes32,uint256[],bool,bytes,uint256[]) claim, uint256 nonce, uint32[] groupMembers) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) NotifyOperatorInactivity(claim FrostInactivityClaim, nonce *big.Int, groupMembers []uint32) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.NotifyOperatorInactivity(&_FrostWalletRegistry.TransactOpts, claim, nonce, groupMembers) +} + +// NotifySeedTimeout is a paid mutator transaction binding the contract method 0xb13b55b2. +// +// Solidity: function notifySeedTimeout() returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) NotifySeedTimeout(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "notifySeedTimeout") +} + +// NotifySeedTimeout is a paid mutator transaction binding the contract method 0xb13b55b2. +// +// Solidity: function notifySeedTimeout() returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) NotifySeedTimeout() (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.NotifySeedTimeout(&_FrostWalletRegistry.TransactOpts) +} + +// NotifySeedTimeout is a paid mutator transaction binding the contract method 0xb13b55b2. +// +// Solidity: function notifySeedTimeout() returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) NotifySeedTimeout() (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.NotifySeedTimeout(&_FrostWalletRegistry.TransactOpts) +} + +// RegisterOperator is a paid mutator transaction binding the contract method 0x3682a450. +// +// Solidity: function registerOperator(address operator) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) RegisterOperator(opts *bind.TransactOpts, operator common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "registerOperator", operator) +} + +// RegisterOperator is a paid mutator transaction binding the contract method 0x3682a450. +// +// Solidity: function registerOperator(address operator) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) RegisterOperator(operator common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.RegisterOperator(&_FrostWalletRegistry.TransactOpts, operator) +} + +// RegisterOperator is a paid mutator transaction binding the contract method 0x3682a450. +// +// Solidity: function registerOperator(address operator) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) RegisterOperator(operator common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.RegisterOperator(&_FrostWalletRegistry.TransactOpts, operator) +} + +// RequestNewWallet is a paid mutator transaction binding the contract method 0x72cc8c6d. +// +// Solidity: function requestNewWallet() returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) RequestNewWallet(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "requestNewWallet") +} + +// RequestNewWallet is a paid mutator transaction binding the contract method 0x72cc8c6d. +// +// Solidity: function requestNewWallet() returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) RequestNewWallet() (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.RequestNewWallet(&_FrostWalletRegistry.TransactOpts) +} + +// RequestNewWallet is a paid mutator transaction binding the contract method 0x72cc8c6d. +// +// Solidity: function requestNewWallet() returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) RequestNewWallet() (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.RequestNewWallet(&_FrostWalletRegistry.TransactOpts) +} + +// Seize is a paid mutator transaction binding the contract method 0xd8dc404d. +// +// Solidity: function seize(uint96 amount, uint256 rewardMultiplier, address notifier, bytes32 walletID, uint32[] walletMembersIDs) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) Seize(opts *bind.TransactOpts, amount *big.Int, rewardMultiplier *big.Int, notifier common.Address, walletID [32]byte, walletMembersIDs []uint32) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "seize", amount, rewardMultiplier, notifier, walletID, walletMembersIDs) +} + +// Seize is a paid mutator transaction binding the contract method 0xd8dc404d. +// +// Solidity: function seize(uint96 amount, uint256 rewardMultiplier, address notifier, bytes32 walletID, uint32[] walletMembersIDs) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) Seize(amount *big.Int, rewardMultiplier *big.Int, notifier common.Address, walletID [32]byte, walletMembersIDs []uint32) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.Seize(&_FrostWalletRegistry.TransactOpts, amount, rewardMultiplier, notifier, walletID, walletMembersIDs) +} + +// Seize is a paid mutator transaction binding the contract method 0xd8dc404d. +// +// Solidity: function seize(uint96 amount, uint256 rewardMultiplier, address notifier, bytes32 walletID, uint32[] walletMembersIDs) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) Seize(amount *big.Int, rewardMultiplier *big.Int, notifier common.Address, walletID [32]byte, walletMembersIDs []uint32) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.Seize(&_FrostWalletRegistry.TransactOpts, amount, rewardMultiplier, notifier, walletID, walletMembersIDs) +} + +// SubmitDkgResult is a paid mutator transaction binding the contract method 0x55129e3a. +// +// Solidity: function submitDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) SubmitDkgResult(opts *bind.TransactOpts, dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "submitDkgResult", dkgResult) +} + +// SubmitDkgResult is a paid mutator transaction binding the contract method 0x55129e3a. +// +// Solidity: function submitDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) SubmitDkgResult(dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.SubmitDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// SubmitDkgResult is a paid mutator transaction binding the contract method 0x55129e3a. +// +// Solidity: function submitDkgResult((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) SubmitDkgResult(dkgResult FrostDkgResult) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.SubmitDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// TransferGovernance is a paid mutator transaction binding the contract method 0xd38bfff4. +// +// Solidity: function transferGovernance(address newGovernance) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) TransferGovernance(opts *bind.TransactOpts, newGovernance common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "transferGovernance", newGovernance) +} + +// TransferGovernance is a paid mutator transaction binding the contract method 0xd38bfff4. +// +// Solidity: function transferGovernance(address newGovernance) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) TransferGovernance(newGovernance common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.TransferGovernance(&_FrostWalletRegistry.TransactOpts, newGovernance) +} + +// TransferGovernance is a paid mutator transaction binding the contract method 0xd38bfff4. +// +// Solidity: function transferGovernance(address newGovernance) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) TransferGovernance(newGovernance common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.TransferGovernance(&_FrostWalletRegistry.TransactOpts, newGovernance) +} + +// UpdateAuthorizationParameters is a paid mutator transaction binding the contract method 0xa04e2980. +// +// Solidity: function updateAuthorizationParameters(uint96 _minimumAuthorization, uint64 _authorizationDecreaseDelay, uint64 _authorizationDecreaseChangePeriod) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateAuthorizationParameters(opts *bind.TransactOpts, _minimumAuthorization *big.Int, _authorizationDecreaseDelay uint64, _authorizationDecreaseChangePeriod uint64) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateAuthorizationParameters", _minimumAuthorization, _authorizationDecreaseDelay, _authorizationDecreaseChangePeriod) +} + +// UpdateAuthorizationParameters is a paid mutator transaction binding the contract method 0xa04e2980. +// +// Solidity: function updateAuthorizationParameters(uint96 _minimumAuthorization, uint64 _authorizationDecreaseDelay, uint64 _authorizationDecreaseChangePeriod) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateAuthorizationParameters(_minimumAuthorization *big.Int, _authorizationDecreaseDelay uint64, _authorizationDecreaseChangePeriod uint64) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateAuthorizationParameters(&_FrostWalletRegistry.TransactOpts, _minimumAuthorization, _authorizationDecreaseDelay, _authorizationDecreaseChangePeriod) +} + +// UpdateAuthorizationParameters is a paid mutator transaction binding the contract method 0xa04e2980. +// +// Solidity: function updateAuthorizationParameters(uint96 _minimumAuthorization, uint64 _authorizationDecreaseDelay, uint64 _authorizationDecreaseChangePeriod) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateAuthorizationParameters(_minimumAuthorization *big.Int, _authorizationDecreaseDelay uint64, _authorizationDecreaseChangePeriod uint64) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateAuthorizationParameters(&_FrostWalletRegistry.TransactOpts, _minimumAuthorization, _authorizationDecreaseDelay, _authorizationDecreaseChangePeriod) +} + +// UpdateDkgParameters is a paid mutator transaction binding the contract method 0x8dcbdf4a. +// +// Solidity: function updateDkgParameters(uint256 _seedTimeout, uint256 _resultChallengePeriodLength, uint256 _resultChallengeExtraGas, uint256 _resultSubmissionTimeout, uint256 _submitterPrecedencePeriodLength) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateDkgParameters(opts *bind.TransactOpts, _seedTimeout *big.Int, _resultChallengePeriodLength *big.Int, _resultChallengeExtraGas *big.Int, _resultSubmissionTimeout *big.Int, _submitterPrecedencePeriodLength *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateDkgParameters", _seedTimeout, _resultChallengePeriodLength, _resultChallengeExtraGas, _resultSubmissionTimeout, _submitterPrecedencePeriodLength) +} + +// UpdateDkgParameters is a paid mutator transaction binding the contract method 0x8dcbdf4a. +// +// Solidity: function updateDkgParameters(uint256 _seedTimeout, uint256 _resultChallengePeriodLength, uint256 _resultChallengeExtraGas, uint256 _resultSubmissionTimeout, uint256 _submitterPrecedencePeriodLength) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateDkgParameters(_seedTimeout *big.Int, _resultChallengePeriodLength *big.Int, _resultChallengeExtraGas *big.Int, _resultSubmissionTimeout *big.Int, _submitterPrecedencePeriodLength *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateDkgParameters(&_FrostWalletRegistry.TransactOpts, _seedTimeout, _resultChallengePeriodLength, _resultChallengeExtraGas, _resultSubmissionTimeout, _submitterPrecedencePeriodLength) +} + +// UpdateDkgParameters is a paid mutator transaction binding the contract method 0x8dcbdf4a. +// +// Solidity: function updateDkgParameters(uint256 _seedTimeout, uint256 _resultChallengePeriodLength, uint256 _resultChallengeExtraGas, uint256 _resultSubmissionTimeout, uint256 _submitterPrecedencePeriodLength) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateDkgParameters(_seedTimeout *big.Int, _resultChallengePeriodLength *big.Int, _resultChallengeExtraGas *big.Int, _resultSubmissionTimeout *big.Int, _submitterPrecedencePeriodLength *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateDkgParameters(&_FrostWalletRegistry.TransactOpts, _seedTimeout, _resultChallengePeriodLength, _resultChallengeExtraGas, _resultSubmissionTimeout, _submitterPrecedencePeriodLength) +} + +// UpdateGasParameters is a paid mutator transaction binding the contract method 0xc88e70f4. +// +// Solidity: function updateGasParameters(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateGasParameters(opts *bind.TransactOpts, dkgResultSubmissionGas *big.Int, dkgResultApprovalGasOffset *big.Int, notifyOperatorInactivityGasOffset *big.Int, notifySeedTimeoutGasOffset *big.Int, notifyDkgTimeoutNegativeGasOffset *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateGasParameters", dkgResultSubmissionGas, dkgResultApprovalGasOffset, notifyOperatorInactivityGasOffset, notifySeedTimeoutGasOffset, notifyDkgTimeoutNegativeGasOffset) +} + +// UpdateGasParameters is a paid mutator transaction binding the contract method 0xc88e70f4. +// +// Solidity: function updateGasParameters(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateGasParameters(dkgResultSubmissionGas *big.Int, dkgResultApprovalGasOffset *big.Int, notifyOperatorInactivityGasOffset *big.Int, notifySeedTimeoutGasOffset *big.Int, notifyDkgTimeoutNegativeGasOffset *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateGasParameters(&_FrostWalletRegistry.TransactOpts, dkgResultSubmissionGas, dkgResultApprovalGasOffset, notifyOperatorInactivityGasOffset, notifySeedTimeoutGasOffset, notifyDkgTimeoutNegativeGasOffset) +} + +// UpdateGasParameters is a paid mutator transaction binding the contract method 0xc88e70f4. +// +// Solidity: function updateGasParameters(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateGasParameters(dkgResultSubmissionGas *big.Int, dkgResultApprovalGasOffset *big.Int, notifyOperatorInactivityGasOffset *big.Int, notifySeedTimeoutGasOffset *big.Int, notifyDkgTimeoutNegativeGasOffset *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateGasParameters(&_FrostWalletRegistry.TransactOpts, dkgResultSubmissionGas, dkgResultApprovalGasOffset, notifyOperatorInactivityGasOffset, notifySeedTimeoutGasOffset, notifyDkgTimeoutNegativeGasOffset) +} + +// UpdateLifecycleOwner is a paid mutator transaction binding the contract method 0x5c776294. +// +// Solidity: function updateLifecycleOwner(address _lifecycleOwner) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateLifecycleOwner(opts *bind.TransactOpts, _lifecycleOwner common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateLifecycleOwner", _lifecycleOwner) +} + +// UpdateLifecycleOwner is a paid mutator transaction binding the contract method 0x5c776294. +// +// Solidity: function updateLifecycleOwner(address _lifecycleOwner) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateLifecycleOwner(_lifecycleOwner common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateLifecycleOwner(&_FrostWalletRegistry.TransactOpts, _lifecycleOwner) +} + +// UpdateLifecycleOwner is a paid mutator transaction binding the contract method 0x5c776294. +// +// Solidity: function updateLifecycleOwner(address _lifecycleOwner) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateLifecycleOwner(_lifecycleOwner common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateLifecycleOwner(&_FrostWalletRegistry.TransactOpts, _lifecycleOwner) +} + +// UpdateOperatorStatus is a paid mutator transaction binding the contract method 0x1c5b0762. +// +// Solidity: function updateOperatorStatus(address operator) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateOperatorStatus(opts *bind.TransactOpts, operator common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateOperatorStatus", operator) +} + +// UpdateOperatorStatus is a paid mutator transaction binding the contract method 0x1c5b0762. +// +// Solidity: function updateOperatorStatus(address operator) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateOperatorStatus(operator common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateOperatorStatus(&_FrostWalletRegistry.TransactOpts, operator) +} + +// UpdateOperatorStatus is a paid mutator transaction binding the contract method 0x1c5b0762. +// +// Solidity: function updateOperatorStatus(address operator) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateOperatorStatus(operator common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateOperatorStatus(&_FrostWalletRegistry.TransactOpts, operator) +} + +// UpdateReimbursementPool is a paid mutator transaction binding the contract method 0x7b35b4e6. +// +// Solidity: function updateReimbursementPool(address _reimbursementPool) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateReimbursementPool(opts *bind.TransactOpts, _reimbursementPool common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateReimbursementPool", _reimbursementPool) +} + +// UpdateReimbursementPool is a paid mutator transaction binding the contract method 0x7b35b4e6. +// +// Solidity: function updateReimbursementPool(address _reimbursementPool) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateReimbursementPool(_reimbursementPool common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateReimbursementPool(&_FrostWalletRegistry.TransactOpts, _reimbursementPool) +} + +// UpdateReimbursementPool is a paid mutator transaction binding the contract method 0x7b35b4e6. +// +// Solidity: function updateReimbursementPool(address _reimbursementPool) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateReimbursementPool(_reimbursementPool common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateReimbursementPool(&_FrostWalletRegistry.TransactOpts, _reimbursementPool) +} + +// UpdateRewardParameters is a paid mutator transaction binding the contract method 0x6c9ecd64. +// +// Solidity: function updateRewardParameters(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateRewardParameters(opts *bind.TransactOpts, maliciousDkgResultNotificationRewardMultiplier *big.Int, sortitionPoolRewardsBanDuration *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateRewardParameters", maliciousDkgResultNotificationRewardMultiplier, sortitionPoolRewardsBanDuration) +} + +// UpdateRewardParameters is a paid mutator transaction binding the contract method 0x6c9ecd64. +// +// Solidity: function updateRewardParameters(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateRewardParameters(maliciousDkgResultNotificationRewardMultiplier *big.Int, sortitionPoolRewardsBanDuration *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateRewardParameters(&_FrostWalletRegistry.TransactOpts, maliciousDkgResultNotificationRewardMultiplier, sortitionPoolRewardsBanDuration) +} + +// UpdateRewardParameters is a paid mutator transaction binding the contract method 0x6c9ecd64. +// +// Solidity: function updateRewardParameters(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateRewardParameters(maliciousDkgResultNotificationRewardMultiplier *big.Int, sortitionPoolRewardsBanDuration *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateRewardParameters(&_FrostWalletRegistry.TransactOpts, maliciousDkgResultNotificationRewardMultiplier, sortitionPoolRewardsBanDuration) +} + +// UpdateSlashingParameters is a paid mutator transaction binding the contract method 0x227fd44f. +// +// Solidity: function updateSlashingParameters(uint96 maliciousDkgResultSlashingAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateSlashingParameters(opts *bind.TransactOpts, maliciousDkgResultSlashingAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateSlashingParameters", maliciousDkgResultSlashingAmount) +} + +// UpdateSlashingParameters is a paid mutator transaction binding the contract method 0x227fd44f. +// +// Solidity: function updateSlashingParameters(uint96 maliciousDkgResultSlashingAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateSlashingParameters(maliciousDkgResultSlashingAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateSlashingParameters(&_FrostWalletRegistry.TransactOpts, maliciousDkgResultSlashingAmount) +} + +// UpdateSlashingParameters is a paid mutator transaction binding the contract method 0x227fd44f. +// +// Solidity: function updateSlashingParameters(uint96 maliciousDkgResultSlashingAmount) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateSlashingParameters(maliciousDkgResultSlashingAmount *big.Int) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateSlashingParameters(&_FrostWalletRegistry.TransactOpts, maliciousDkgResultSlashingAmount) +} + +// UpdateWalletOwner is a paid mutator transaction binding the contract method 0xd0bcc0e3. +// +// Solidity: function updateWalletOwner(address _walletOwner) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpdateWalletOwner(opts *bind.TransactOpts, _walletOwner common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "updateWalletOwner", _walletOwner) +} + +// UpdateWalletOwner is a paid mutator transaction binding the contract method 0xd0bcc0e3. +// +// Solidity: function updateWalletOwner(address _walletOwner) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpdateWalletOwner(_walletOwner common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateWalletOwner(&_FrostWalletRegistry.TransactOpts, _walletOwner) +} + +// UpdateWalletOwner is a paid mutator transaction binding the contract method 0xd0bcc0e3. +// +// Solidity: function updateWalletOwner(address _walletOwner) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpdateWalletOwner(_walletOwner common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpdateWalletOwner(&_FrostWalletRegistry.TransactOpts, _walletOwner) +} + +// UpgradeRandomBeacon is a paid mutator transaction binding the contract method 0x6b5f2bff. +// +// Solidity: function upgradeRandomBeacon(address _randomBeacon) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) UpgradeRandomBeacon(opts *bind.TransactOpts, _randomBeacon common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "upgradeRandomBeacon", _randomBeacon) +} + +// UpgradeRandomBeacon is a paid mutator transaction binding the contract method 0x6b5f2bff. +// +// Solidity: function upgradeRandomBeacon(address _randomBeacon) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) UpgradeRandomBeacon(_randomBeacon common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpgradeRandomBeacon(&_FrostWalletRegistry.TransactOpts, _randomBeacon) +} + +// UpgradeRandomBeacon is a paid mutator transaction binding the contract method 0x6b5f2bff. +// +// Solidity: function upgradeRandomBeacon(address _randomBeacon) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) UpgradeRandomBeacon(_randomBeacon common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.UpgradeRandomBeacon(&_FrostWalletRegistry.TransactOpts, _randomBeacon) +} + +// WithdrawIneligibleRewards is a paid mutator transaction binding the contract method 0x663032cd. +// +// Solidity: function withdrawIneligibleRewards(address recipient) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) WithdrawIneligibleRewards(opts *bind.TransactOpts, recipient common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "withdrawIneligibleRewards", recipient) +} + +// WithdrawIneligibleRewards is a paid mutator transaction binding the contract method 0x663032cd. +// +// Solidity: function withdrawIneligibleRewards(address recipient) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) WithdrawIneligibleRewards(recipient common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.WithdrawIneligibleRewards(&_FrostWalletRegistry.TransactOpts, recipient) +} + +// WithdrawIneligibleRewards is a paid mutator transaction binding the contract method 0x663032cd. +// +// Solidity: function withdrawIneligibleRewards(address recipient) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) WithdrawIneligibleRewards(recipient common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.WithdrawIneligibleRewards(&_FrostWalletRegistry.TransactOpts, recipient) +} + +// WithdrawRewards is a paid mutator transaction binding the contract method 0x42d86693. +// +// Solidity: function withdrawRewards(address stakingProvider) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) WithdrawRewards(opts *bind.TransactOpts, stakingProvider common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "withdrawRewards", stakingProvider) +} + +// WithdrawRewards is a paid mutator transaction binding the contract method 0x42d86693. +// +// Solidity: function withdrawRewards(address stakingProvider) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) WithdrawRewards(stakingProvider common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.WithdrawRewards(&_FrostWalletRegistry.TransactOpts, stakingProvider) +} + +// WithdrawRewards is a paid mutator transaction binding the contract method 0x42d86693. +// +// Solidity: function withdrawRewards(address stakingProvider) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) WithdrawRewards(stakingProvider common.Address) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.WithdrawRewards(&_FrostWalletRegistry.TransactOpts, stakingProvider) +} + +// FrostWalletRegistryAuthorizationDecreaseApprovedIterator is returned from FilterAuthorizationDecreaseApproved and is used to iterate over the raw logs and unpacked data for AuthorizationDecreaseApproved events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryAuthorizationDecreaseApprovedIterator struct { + Event *FrostWalletRegistryAuthorizationDecreaseApproved // 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 *FrostWalletRegistryAuthorizationDecreaseApprovedIterator) 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(FrostWalletRegistryAuthorizationDecreaseApproved) + 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(FrostWalletRegistryAuthorizationDecreaseApproved) + 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 *FrostWalletRegistryAuthorizationDecreaseApprovedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryAuthorizationDecreaseApprovedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryAuthorizationDecreaseApproved represents a AuthorizationDecreaseApproved event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryAuthorizationDecreaseApproved struct { + StakingProvider common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterAuthorizationDecreaseApproved is a free log retrieval operation binding the contract event 0x50270a522c2fef97b6b7385c2aa4a4518adda681530e0a1fe9f5e840f6f2cd9d. +// +// Solidity: event AuthorizationDecreaseApproved(address indexed stakingProvider) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterAuthorizationDecreaseApproved(opts *bind.FilterOpts, stakingProvider []common.Address) (*FrostWalletRegistryAuthorizationDecreaseApprovedIterator, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "AuthorizationDecreaseApproved", stakingProviderRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryAuthorizationDecreaseApprovedIterator{contract: _FrostWalletRegistry.contract, event: "AuthorizationDecreaseApproved", logs: logs, sub: sub}, nil +} + +// WatchAuthorizationDecreaseApproved is a free log subscription operation binding the contract event 0x50270a522c2fef97b6b7385c2aa4a4518adda681530e0a1fe9f5e840f6f2cd9d. +// +// Solidity: event AuthorizationDecreaseApproved(address indexed stakingProvider) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchAuthorizationDecreaseApproved(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryAuthorizationDecreaseApproved, stakingProvider []common.Address) (event.Subscription, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "AuthorizationDecreaseApproved", stakingProviderRule) + 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(FrostWalletRegistryAuthorizationDecreaseApproved) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "AuthorizationDecreaseApproved", 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 +} + +// ParseAuthorizationDecreaseApproved is a log parse operation binding the contract event 0x50270a522c2fef97b6b7385c2aa4a4518adda681530e0a1fe9f5e840f6f2cd9d. +// +// Solidity: event AuthorizationDecreaseApproved(address indexed stakingProvider) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseAuthorizationDecreaseApproved(log types.Log) (*FrostWalletRegistryAuthorizationDecreaseApproved, error) { + event := new(FrostWalletRegistryAuthorizationDecreaseApproved) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "AuthorizationDecreaseApproved", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryAuthorizationDecreaseRequestedIterator is returned from FilterAuthorizationDecreaseRequested and is used to iterate over the raw logs and unpacked data for AuthorizationDecreaseRequested events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryAuthorizationDecreaseRequestedIterator struct { + Event *FrostWalletRegistryAuthorizationDecreaseRequested // 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 *FrostWalletRegistryAuthorizationDecreaseRequestedIterator) 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(FrostWalletRegistryAuthorizationDecreaseRequested) + 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(FrostWalletRegistryAuthorizationDecreaseRequested) + 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 *FrostWalletRegistryAuthorizationDecreaseRequestedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryAuthorizationDecreaseRequestedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryAuthorizationDecreaseRequested represents a AuthorizationDecreaseRequested event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryAuthorizationDecreaseRequested struct { + StakingProvider common.Address + Operator common.Address + FromAmount *big.Int + ToAmount *big.Int + DecreasingAt uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterAuthorizationDecreaseRequested is a free log retrieval operation binding the contract event 0x545cbf267cef6fe43f11f6219417ab43a0e8e345adbaae5f626d9bc325e8535a. +// +// Solidity: event AuthorizationDecreaseRequested(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount, uint64 decreasingAt) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterAuthorizationDecreaseRequested(opts *bind.FilterOpts, stakingProvider []common.Address, operator []common.Address) (*FrostWalletRegistryAuthorizationDecreaseRequestedIterator, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "AuthorizationDecreaseRequested", stakingProviderRule, operatorRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryAuthorizationDecreaseRequestedIterator{contract: _FrostWalletRegistry.contract, event: "AuthorizationDecreaseRequested", logs: logs, sub: sub}, nil +} + +// WatchAuthorizationDecreaseRequested is a free log subscription operation binding the contract event 0x545cbf267cef6fe43f11f6219417ab43a0e8e345adbaae5f626d9bc325e8535a. +// +// Solidity: event AuthorizationDecreaseRequested(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount, uint64 decreasingAt) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchAuthorizationDecreaseRequested(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryAuthorizationDecreaseRequested, stakingProvider []common.Address, operator []common.Address) (event.Subscription, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "AuthorizationDecreaseRequested", stakingProviderRule, operatorRule) + 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(FrostWalletRegistryAuthorizationDecreaseRequested) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "AuthorizationDecreaseRequested", 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 +} + +// ParseAuthorizationDecreaseRequested is a log parse operation binding the contract event 0x545cbf267cef6fe43f11f6219417ab43a0e8e345adbaae5f626d9bc325e8535a. +// +// Solidity: event AuthorizationDecreaseRequested(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount, uint64 decreasingAt) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseAuthorizationDecreaseRequested(log types.Log) (*FrostWalletRegistryAuthorizationDecreaseRequested, error) { + event := new(FrostWalletRegistryAuthorizationDecreaseRequested) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "AuthorizationDecreaseRequested", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryAuthorizationIncreasedIterator is returned from FilterAuthorizationIncreased and is used to iterate over the raw logs and unpacked data for AuthorizationIncreased events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryAuthorizationIncreasedIterator struct { + Event *FrostWalletRegistryAuthorizationIncreased // 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 *FrostWalletRegistryAuthorizationIncreasedIterator) 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(FrostWalletRegistryAuthorizationIncreased) + 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(FrostWalletRegistryAuthorizationIncreased) + 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 *FrostWalletRegistryAuthorizationIncreasedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryAuthorizationIncreasedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryAuthorizationIncreased represents a AuthorizationIncreased event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryAuthorizationIncreased struct { + StakingProvider common.Address + Operator common.Address + FromAmount *big.Int + ToAmount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterAuthorizationIncreased is a free log retrieval operation binding the contract event 0x87f9f9f59204f53d57a89a817c6083a17979cd0531791c91e18551a56e3cfdd7. +// +// Solidity: event AuthorizationIncreased(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterAuthorizationIncreased(opts *bind.FilterOpts, stakingProvider []common.Address, operator []common.Address) (*FrostWalletRegistryAuthorizationIncreasedIterator, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "AuthorizationIncreased", stakingProviderRule, operatorRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryAuthorizationIncreasedIterator{contract: _FrostWalletRegistry.contract, event: "AuthorizationIncreased", logs: logs, sub: sub}, nil +} + +// WatchAuthorizationIncreased is a free log subscription operation binding the contract event 0x87f9f9f59204f53d57a89a817c6083a17979cd0531791c91e18551a56e3cfdd7. +// +// Solidity: event AuthorizationIncreased(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchAuthorizationIncreased(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryAuthorizationIncreased, stakingProvider []common.Address, operator []common.Address) (event.Subscription, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "AuthorizationIncreased", stakingProviderRule, operatorRule) + 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(FrostWalletRegistryAuthorizationIncreased) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "AuthorizationIncreased", 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 +} + +// ParseAuthorizationIncreased is a log parse operation binding the contract event 0x87f9f9f59204f53d57a89a817c6083a17979cd0531791c91e18551a56e3cfdd7. +// +// Solidity: event AuthorizationIncreased(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseAuthorizationIncreased(log types.Log) (*FrostWalletRegistryAuthorizationIncreased, error) { + event := new(FrostWalletRegistryAuthorizationIncreased) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "AuthorizationIncreased", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryAuthorizationParametersUpdatedIterator is returned from FilterAuthorizationParametersUpdated and is used to iterate over the raw logs and unpacked data for AuthorizationParametersUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryAuthorizationParametersUpdatedIterator struct { + Event *FrostWalletRegistryAuthorizationParametersUpdated // 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 *FrostWalletRegistryAuthorizationParametersUpdatedIterator) 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(FrostWalletRegistryAuthorizationParametersUpdated) + 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(FrostWalletRegistryAuthorizationParametersUpdated) + 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 *FrostWalletRegistryAuthorizationParametersUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryAuthorizationParametersUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryAuthorizationParametersUpdated represents a AuthorizationParametersUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryAuthorizationParametersUpdated struct { + MinimumAuthorization *big.Int + AuthorizationDecreaseDelay uint64 + AuthorizationDecreaseChangePeriod uint64 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterAuthorizationParametersUpdated is a free log retrieval operation binding the contract event 0x544b726e42801bb47073854eeedae851903f66fe32a5bd24e626e10b90027b51. +// +// Solidity: event AuthorizationParametersUpdated(uint96 minimumAuthorization, uint64 authorizationDecreaseDelay, uint64 authorizationDecreaseChangePeriod) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterAuthorizationParametersUpdated(opts *bind.FilterOpts) (*FrostWalletRegistryAuthorizationParametersUpdatedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "AuthorizationParametersUpdated") + if err != nil { + return nil, err + } + return &FrostWalletRegistryAuthorizationParametersUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "AuthorizationParametersUpdated", logs: logs, sub: sub}, nil +} + +// WatchAuthorizationParametersUpdated is a free log subscription operation binding the contract event 0x544b726e42801bb47073854eeedae851903f66fe32a5bd24e626e10b90027b51. +// +// Solidity: event AuthorizationParametersUpdated(uint96 minimumAuthorization, uint64 authorizationDecreaseDelay, uint64 authorizationDecreaseChangePeriod) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchAuthorizationParametersUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryAuthorizationParametersUpdated) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "AuthorizationParametersUpdated") + 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(FrostWalletRegistryAuthorizationParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "AuthorizationParametersUpdated", 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 +} + +// ParseAuthorizationParametersUpdated is a log parse operation binding the contract event 0x544b726e42801bb47073854eeedae851903f66fe32a5bd24e626e10b90027b51. +// +// Solidity: event AuthorizationParametersUpdated(uint96 minimumAuthorization, uint64 authorizationDecreaseDelay, uint64 authorizationDecreaseChangePeriod) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseAuthorizationParametersUpdated(log types.Log) (*FrostWalletRegistryAuthorizationParametersUpdated, error) { + event := new(FrostWalletRegistryAuthorizationParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "AuthorizationParametersUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgMaliciousResultSlashedIterator is returned from FilterDkgMaliciousResultSlashed and is used to iterate over the raw logs and unpacked data for DkgMaliciousResultSlashed events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgMaliciousResultSlashedIterator struct { + Event *FrostWalletRegistryDkgMaliciousResultSlashed // 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 *FrostWalletRegistryDkgMaliciousResultSlashedIterator) 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(FrostWalletRegistryDkgMaliciousResultSlashed) + 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(FrostWalletRegistryDkgMaliciousResultSlashed) + 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 *FrostWalletRegistryDkgMaliciousResultSlashedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgMaliciousResultSlashedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgMaliciousResultSlashed represents a DkgMaliciousResultSlashed event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgMaliciousResultSlashed struct { + ResultHash [32]byte + SlashingAmount *big.Int + MaliciousSubmitter common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgMaliciousResultSlashed is a free log retrieval operation binding the contract event 0x88f76c659db78142f88e94db3ca791869495394c6c1b3d412ced9022dc97c9e3. +// +// Solidity: event DkgMaliciousResultSlashed(bytes32 indexed resultHash, uint256 slashingAmount, address maliciousSubmitter) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgMaliciousResultSlashed(opts *bind.FilterOpts, resultHash [][32]byte) (*FrostWalletRegistryDkgMaliciousResultSlashedIterator, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgMaliciousResultSlashed", resultHashRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgMaliciousResultSlashedIterator{contract: _FrostWalletRegistry.contract, event: "DkgMaliciousResultSlashed", logs: logs, sub: sub}, nil +} + +// WatchDkgMaliciousResultSlashed is a free log subscription operation binding the contract event 0x88f76c659db78142f88e94db3ca791869495394c6c1b3d412ced9022dc97c9e3. +// +// Solidity: event DkgMaliciousResultSlashed(bytes32 indexed resultHash, uint256 slashingAmount, address maliciousSubmitter) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgMaliciousResultSlashed(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgMaliciousResultSlashed, resultHash [][32]byte) (event.Subscription, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgMaliciousResultSlashed", resultHashRule) + 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(FrostWalletRegistryDkgMaliciousResultSlashed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgMaliciousResultSlashed", 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 +} + +// ParseDkgMaliciousResultSlashed is a log parse operation binding the contract event 0x88f76c659db78142f88e94db3ca791869495394c6c1b3d412ced9022dc97c9e3. +// +// Solidity: event DkgMaliciousResultSlashed(bytes32 indexed resultHash, uint256 slashingAmount, address maliciousSubmitter) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgMaliciousResultSlashed(log types.Log) (*FrostWalletRegistryDkgMaliciousResultSlashed, error) { + event := new(FrostWalletRegistryDkgMaliciousResultSlashed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgMaliciousResultSlashed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgMaliciousResultSlashingFailedIterator is returned from FilterDkgMaliciousResultSlashingFailed and is used to iterate over the raw logs and unpacked data for DkgMaliciousResultSlashingFailed events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgMaliciousResultSlashingFailedIterator struct { + Event *FrostWalletRegistryDkgMaliciousResultSlashingFailed // 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 *FrostWalletRegistryDkgMaliciousResultSlashingFailedIterator) 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(FrostWalletRegistryDkgMaliciousResultSlashingFailed) + 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(FrostWalletRegistryDkgMaliciousResultSlashingFailed) + 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 *FrostWalletRegistryDkgMaliciousResultSlashingFailedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgMaliciousResultSlashingFailedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgMaliciousResultSlashingFailed represents a DkgMaliciousResultSlashingFailed event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgMaliciousResultSlashingFailed struct { + ResultHash [32]byte + SlashingAmount *big.Int + MaliciousSubmitter common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgMaliciousResultSlashingFailed is a free log retrieval operation binding the contract event 0x14621289a12ab59e0737decc388bba91d929c723defb4682d5d19b9a12ecfecb. +// +// Solidity: event DkgMaliciousResultSlashingFailed(bytes32 indexed resultHash, uint256 slashingAmount, address maliciousSubmitter) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgMaliciousResultSlashingFailed(opts *bind.FilterOpts, resultHash [][32]byte) (*FrostWalletRegistryDkgMaliciousResultSlashingFailedIterator, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgMaliciousResultSlashingFailed", resultHashRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgMaliciousResultSlashingFailedIterator{contract: _FrostWalletRegistry.contract, event: "DkgMaliciousResultSlashingFailed", logs: logs, sub: sub}, nil +} + +// WatchDkgMaliciousResultSlashingFailed is a free log subscription operation binding the contract event 0x14621289a12ab59e0737decc388bba91d929c723defb4682d5d19b9a12ecfecb. +// +// Solidity: event DkgMaliciousResultSlashingFailed(bytes32 indexed resultHash, uint256 slashingAmount, address maliciousSubmitter) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgMaliciousResultSlashingFailed(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgMaliciousResultSlashingFailed, resultHash [][32]byte) (event.Subscription, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgMaliciousResultSlashingFailed", resultHashRule) + 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(FrostWalletRegistryDkgMaliciousResultSlashingFailed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgMaliciousResultSlashingFailed", 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 +} + +// ParseDkgMaliciousResultSlashingFailed is a log parse operation binding the contract event 0x14621289a12ab59e0737decc388bba91d929c723defb4682d5d19b9a12ecfecb. +// +// Solidity: event DkgMaliciousResultSlashingFailed(bytes32 indexed resultHash, uint256 slashingAmount, address maliciousSubmitter) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgMaliciousResultSlashingFailed(log types.Log) (*FrostWalletRegistryDkgMaliciousResultSlashingFailed, error) { + event := new(FrostWalletRegistryDkgMaliciousResultSlashingFailed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgMaliciousResultSlashingFailed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgParametersUpdatedIterator is returned from FilterDkgParametersUpdated and is used to iterate over the raw logs and unpacked data for DkgParametersUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgParametersUpdatedIterator struct { + Event *FrostWalletRegistryDkgParametersUpdated // 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 *FrostWalletRegistryDkgParametersUpdatedIterator) 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(FrostWalletRegistryDkgParametersUpdated) + 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(FrostWalletRegistryDkgParametersUpdated) + 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 *FrostWalletRegistryDkgParametersUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgParametersUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgParametersUpdated represents a DkgParametersUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgParametersUpdated struct { + SeedTimeout *big.Int + ResultChallengePeriodLength *big.Int + ResultChallengeExtraGas *big.Int + ResultSubmissionTimeout *big.Int + ResultSubmitterPrecedencePeriodLength *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgParametersUpdated is a free log retrieval operation binding the contract event 0x59ae8ed7b3a7e5f6dde4cff478f0ac0aa652c5edc4f4757b09a778a430b02c56. +// +// Solidity: event DkgParametersUpdated(uint256 seedTimeout, uint256 resultChallengePeriodLength, uint256 resultChallengeExtraGas, uint256 resultSubmissionTimeout, uint256 resultSubmitterPrecedencePeriodLength) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgParametersUpdated(opts *bind.FilterOpts) (*FrostWalletRegistryDkgParametersUpdatedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgParametersUpdated") + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgParametersUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "DkgParametersUpdated", logs: logs, sub: sub}, nil +} + +// WatchDkgParametersUpdated is a free log subscription operation binding the contract event 0x59ae8ed7b3a7e5f6dde4cff478f0ac0aa652c5edc4f4757b09a778a430b02c56. +// +// Solidity: event DkgParametersUpdated(uint256 seedTimeout, uint256 resultChallengePeriodLength, uint256 resultChallengeExtraGas, uint256 resultSubmissionTimeout, uint256 resultSubmitterPrecedencePeriodLength) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgParametersUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgParametersUpdated) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgParametersUpdated") + 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(FrostWalletRegistryDkgParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgParametersUpdated", 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 +} + +// ParseDkgParametersUpdated is a log parse operation binding the contract event 0x59ae8ed7b3a7e5f6dde4cff478f0ac0aa652c5edc4f4757b09a778a430b02c56. +// +// Solidity: event DkgParametersUpdated(uint256 seedTimeout, uint256 resultChallengePeriodLength, uint256 resultChallengeExtraGas, uint256 resultSubmissionTimeout, uint256 resultSubmitterPrecedencePeriodLength) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgParametersUpdated(log types.Log) (*FrostWalletRegistryDkgParametersUpdated, error) { + event := new(FrostWalletRegistryDkgParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgParametersUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgResultApprovedIterator is returned from FilterDkgResultApproved and is used to iterate over the raw logs and unpacked data for DkgResultApproved events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgResultApprovedIterator struct { + Event *FrostWalletRegistryDkgResultApproved // 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 *FrostWalletRegistryDkgResultApprovedIterator) 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(FrostWalletRegistryDkgResultApproved) + 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(FrostWalletRegistryDkgResultApproved) + 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 *FrostWalletRegistryDkgResultApprovedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgResultApprovedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgResultApproved represents a DkgResultApproved event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgResultApproved struct { + ResultHash [32]byte + Approver common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgResultApproved is a free log retrieval operation binding the contract event 0xe6e9d5eba171e82025efb3f3d44fd35905e7283d104284cb9f3bbc5bf1e4276f. +// +// Solidity: event DkgResultApproved(bytes32 indexed resultHash, address indexed approver) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgResultApproved(opts *bind.FilterOpts, resultHash [][32]byte, approver []common.Address) (*FrostWalletRegistryDkgResultApprovedIterator, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + var approverRule []interface{} + for _, approverItem := range approver { + approverRule = append(approverRule, approverItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgResultApproved", resultHashRule, approverRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgResultApprovedIterator{contract: _FrostWalletRegistry.contract, event: "DkgResultApproved", logs: logs, sub: sub}, nil +} + +// WatchDkgResultApproved is a free log subscription operation binding the contract event 0xe6e9d5eba171e82025efb3f3d44fd35905e7283d104284cb9f3bbc5bf1e4276f. +// +// Solidity: event DkgResultApproved(bytes32 indexed resultHash, address indexed approver) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgResultApproved(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgResultApproved, resultHash [][32]byte, approver []common.Address) (event.Subscription, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + var approverRule []interface{} + for _, approverItem := range approver { + approverRule = append(approverRule, approverItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgResultApproved", resultHashRule, approverRule) + 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(FrostWalletRegistryDkgResultApproved) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgResultApproved", 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 +} + +// ParseDkgResultApproved is a log parse operation binding the contract event 0xe6e9d5eba171e82025efb3f3d44fd35905e7283d104284cb9f3bbc5bf1e4276f. +// +// Solidity: event DkgResultApproved(bytes32 indexed resultHash, address indexed approver) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgResultApproved(log types.Log) (*FrostWalletRegistryDkgResultApproved, error) { + event := new(FrostWalletRegistryDkgResultApproved) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgResultApproved", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgResultChallengedIterator is returned from FilterDkgResultChallenged and is used to iterate over the raw logs and unpacked data for DkgResultChallenged events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgResultChallengedIterator struct { + Event *FrostWalletRegistryDkgResultChallenged // 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 *FrostWalletRegistryDkgResultChallengedIterator) 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(FrostWalletRegistryDkgResultChallenged) + 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(FrostWalletRegistryDkgResultChallenged) + 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 *FrostWalletRegistryDkgResultChallengedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgResultChallengedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgResultChallenged represents a DkgResultChallenged event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgResultChallenged struct { + ResultHash [32]byte + Challenger common.Address + Reason string + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgResultChallenged is a free log retrieval operation binding the contract event 0x703feb01415a2995816e8d082fd7aad0eacada1a2f63fdb3226e47f8a0285436. +// +// Solidity: event DkgResultChallenged(bytes32 indexed resultHash, address indexed challenger, string reason) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgResultChallenged(opts *bind.FilterOpts, resultHash [][32]byte, challenger []common.Address) (*FrostWalletRegistryDkgResultChallengedIterator, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + var challengerRule []interface{} + for _, challengerItem := range challenger { + challengerRule = append(challengerRule, challengerItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgResultChallenged", resultHashRule, challengerRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgResultChallengedIterator{contract: _FrostWalletRegistry.contract, event: "DkgResultChallenged", logs: logs, sub: sub}, nil +} + +// WatchDkgResultChallenged is a free log subscription operation binding the contract event 0x703feb01415a2995816e8d082fd7aad0eacada1a2f63fdb3226e47f8a0285436. +// +// Solidity: event DkgResultChallenged(bytes32 indexed resultHash, address indexed challenger, string reason) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgResultChallenged(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgResultChallenged, resultHash [][32]byte, challenger []common.Address) (event.Subscription, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + var challengerRule []interface{} + for _, challengerItem := range challenger { + challengerRule = append(challengerRule, challengerItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgResultChallenged", resultHashRule, challengerRule) + 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(FrostWalletRegistryDkgResultChallenged) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgResultChallenged", 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 +} + +// ParseDkgResultChallenged is a log parse operation binding the contract event 0x703feb01415a2995816e8d082fd7aad0eacada1a2f63fdb3226e47f8a0285436. +// +// Solidity: event DkgResultChallenged(bytes32 indexed resultHash, address indexed challenger, string reason) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgResultChallenged(log types.Log) (*FrostWalletRegistryDkgResultChallenged, error) { + event := new(FrostWalletRegistryDkgResultChallenged) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgResultChallenged", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgResultSubmittedIterator is returned from FilterDkgResultSubmitted and is used to iterate over the raw logs and unpacked data for DkgResultSubmitted events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgResultSubmittedIterator struct { + Event *FrostWalletRegistryDkgResultSubmitted // 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 *FrostWalletRegistryDkgResultSubmittedIterator) 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(FrostWalletRegistryDkgResultSubmitted) + 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(FrostWalletRegistryDkgResultSubmitted) + 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 *FrostWalletRegistryDkgResultSubmittedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgResultSubmittedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgResultSubmitted represents a DkgResultSubmitted event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgResultSubmitted struct { + ResultHash [32]byte + Seed *big.Int + Result FrostDkgResult + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgResultSubmitted is a free log retrieval operation binding the contract event 0xbfc6cd6291b6741d3ac1631ba81a0288d08265bea4d59d452e8c953e11ec11c6. +// +// Solidity: event DkgResultSubmitted(bytes32 indexed resultHash, uint256 indexed seed, (uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgResultSubmitted(opts *bind.FilterOpts, resultHash [][32]byte, seed []*big.Int) (*FrostWalletRegistryDkgResultSubmittedIterator, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + var seedRule []interface{} + for _, seedItem := range seed { + seedRule = append(seedRule, seedItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgResultSubmitted", resultHashRule, seedRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgResultSubmittedIterator{contract: _FrostWalletRegistry.contract, event: "DkgResultSubmitted", logs: logs, sub: sub}, nil +} + +// WatchDkgResultSubmitted is a free log subscription operation binding the contract event 0xbfc6cd6291b6741d3ac1631ba81a0288d08265bea4d59d452e8c953e11ec11c6. +// +// Solidity: event DkgResultSubmitted(bytes32 indexed resultHash, uint256 indexed seed, (uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgResultSubmitted(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgResultSubmitted, resultHash [][32]byte, seed []*big.Int) (event.Subscription, error) { + + var resultHashRule []interface{} + for _, resultHashItem := range resultHash { + resultHashRule = append(resultHashRule, resultHashItem) + } + var seedRule []interface{} + for _, seedItem := range seed { + seedRule = append(seedRule, seedItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgResultSubmitted", resultHashRule, seedRule) + 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(FrostWalletRegistryDkgResultSubmitted) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgResultSubmitted", 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 +} + +// ParseDkgResultSubmitted is a log parse operation binding the contract event 0xbfc6cd6291b6741d3ac1631ba81a0288d08265bea4d59d452e8c953e11ec11c6. +// +// Solidity: event DkgResultSubmitted(bytes32 indexed resultHash, uint256 indexed seed, (uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgResultSubmitted(log types.Log) (*FrostWalletRegistryDkgResultSubmitted, error) { + event := new(FrostWalletRegistryDkgResultSubmitted) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgResultSubmitted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgSeedTimedOutIterator is returned from FilterDkgSeedTimedOut and is used to iterate over the raw logs and unpacked data for DkgSeedTimedOut events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgSeedTimedOutIterator struct { + Event *FrostWalletRegistryDkgSeedTimedOut // 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 *FrostWalletRegistryDkgSeedTimedOutIterator) 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(FrostWalletRegistryDkgSeedTimedOut) + 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(FrostWalletRegistryDkgSeedTimedOut) + 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 *FrostWalletRegistryDkgSeedTimedOutIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgSeedTimedOutIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgSeedTimedOut represents a DkgSeedTimedOut event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgSeedTimedOut struct { + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgSeedTimedOut is a free log retrieval operation binding the contract event 0x68c52f05452e81639fa06f379aee3178cddee4725521fff886f244c99e868b50. +// +// Solidity: event DkgSeedTimedOut() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgSeedTimedOut(opts *bind.FilterOpts) (*FrostWalletRegistryDkgSeedTimedOutIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgSeedTimedOut") + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgSeedTimedOutIterator{contract: _FrostWalletRegistry.contract, event: "DkgSeedTimedOut", logs: logs, sub: sub}, nil +} + +// WatchDkgSeedTimedOut is a free log subscription operation binding the contract event 0x68c52f05452e81639fa06f379aee3178cddee4725521fff886f244c99e868b50. +// +// Solidity: event DkgSeedTimedOut() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgSeedTimedOut(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgSeedTimedOut) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgSeedTimedOut") + 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(FrostWalletRegistryDkgSeedTimedOut) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgSeedTimedOut", 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 +} + +// ParseDkgSeedTimedOut is a log parse operation binding the contract event 0x68c52f05452e81639fa06f379aee3178cddee4725521fff886f244c99e868b50. +// +// Solidity: event DkgSeedTimedOut() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgSeedTimedOut(log types.Log) (*FrostWalletRegistryDkgSeedTimedOut, error) { + event := new(FrostWalletRegistryDkgSeedTimedOut) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgSeedTimedOut", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgStartedIterator is returned from FilterDkgStarted and is used to iterate over the raw logs and unpacked data for DkgStarted events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgStartedIterator struct { + Event *FrostWalletRegistryDkgStarted // 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 *FrostWalletRegistryDkgStartedIterator) 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(FrostWalletRegistryDkgStarted) + 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(FrostWalletRegistryDkgStarted) + 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 *FrostWalletRegistryDkgStartedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgStartedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgStarted represents a DkgStarted event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgStarted struct { + Seed *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgStarted is a free log retrieval operation binding the contract event 0xb2ad26c2940889d79df2ee9c758a8aefa00c5ca90eee119af0e5d795df3b98bb. +// +// Solidity: event DkgStarted(uint256 indexed seed) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgStarted(opts *bind.FilterOpts, seed []*big.Int) (*FrostWalletRegistryDkgStartedIterator, error) { + + var seedRule []interface{} + for _, seedItem := range seed { + seedRule = append(seedRule, seedItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgStarted", seedRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgStartedIterator{contract: _FrostWalletRegistry.contract, event: "DkgStarted", logs: logs, sub: sub}, nil +} + +// WatchDkgStarted is a free log subscription operation binding the contract event 0xb2ad26c2940889d79df2ee9c758a8aefa00c5ca90eee119af0e5d795df3b98bb. +// +// Solidity: event DkgStarted(uint256 indexed seed) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgStarted(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgStarted, seed []*big.Int) (event.Subscription, error) { + + var seedRule []interface{} + for _, seedItem := range seed { + seedRule = append(seedRule, seedItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgStarted", seedRule) + 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(FrostWalletRegistryDkgStarted) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgStarted", 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 +} + +// ParseDkgStarted is a log parse operation binding the contract event 0xb2ad26c2940889d79df2ee9c758a8aefa00c5ca90eee119af0e5d795df3b98bb. +// +// Solidity: event DkgStarted(uint256 indexed seed) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgStarted(log types.Log) (*FrostWalletRegistryDkgStarted, error) { + event := new(FrostWalletRegistryDkgStarted) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgStarted", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgStateLockedIterator is returned from FilterDkgStateLocked and is used to iterate over the raw logs and unpacked data for DkgStateLocked events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgStateLockedIterator struct { + Event *FrostWalletRegistryDkgStateLocked // 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 *FrostWalletRegistryDkgStateLockedIterator) 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(FrostWalletRegistryDkgStateLocked) + 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(FrostWalletRegistryDkgStateLocked) + 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 *FrostWalletRegistryDkgStateLockedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgStateLockedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgStateLocked represents a DkgStateLocked event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgStateLocked struct { + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgStateLocked is a free log retrieval operation binding the contract event 0x5c3ed2397d4d21298b2fb5027ac8e2d42e3c9c72bbb55ddb030e2a36a0cdff6b. +// +// Solidity: event DkgStateLocked() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgStateLocked(opts *bind.FilterOpts) (*FrostWalletRegistryDkgStateLockedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgStateLocked") + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgStateLockedIterator{contract: _FrostWalletRegistry.contract, event: "DkgStateLocked", logs: logs, sub: sub}, nil +} + +// WatchDkgStateLocked is a free log subscription operation binding the contract event 0x5c3ed2397d4d21298b2fb5027ac8e2d42e3c9c72bbb55ddb030e2a36a0cdff6b. +// +// Solidity: event DkgStateLocked() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgStateLocked(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgStateLocked) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgStateLocked") + 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(FrostWalletRegistryDkgStateLocked) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgStateLocked", 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 +} + +// ParseDkgStateLocked is a log parse operation binding the contract event 0x5c3ed2397d4d21298b2fb5027ac8e2d42e3c9c72bbb55ddb030e2a36a0cdff6b. +// +// Solidity: event DkgStateLocked() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgStateLocked(log types.Log) (*FrostWalletRegistryDkgStateLocked, error) { + event := new(FrostWalletRegistryDkgStateLocked) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgStateLocked", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryDkgTimedOutIterator is returned from FilterDkgTimedOut and is used to iterate over the raw logs and unpacked data for DkgTimedOut events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgTimedOutIterator struct { + Event *FrostWalletRegistryDkgTimedOut // 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 *FrostWalletRegistryDkgTimedOutIterator) 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(FrostWalletRegistryDkgTimedOut) + 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(FrostWalletRegistryDkgTimedOut) + 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 *FrostWalletRegistryDkgTimedOutIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryDkgTimedOutIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryDkgTimedOut represents a DkgTimedOut event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryDkgTimedOut struct { + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgTimedOut is a free log retrieval operation binding the contract event 0x2852b3e178dd281713b041c3d90b4815bb55b7ec812931d1e8e8d8bb2ed72d3e. +// +// Solidity: event DkgTimedOut() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterDkgTimedOut(opts *bind.FilterOpts) (*FrostWalletRegistryDkgTimedOutIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "DkgTimedOut") + if err != nil { + return nil, err + } + return &FrostWalletRegistryDkgTimedOutIterator{contract: _FrostWalletRegistry.contract, event: "DkgTimedOut", logs: logs, sub: sub}, nil +} + +// WatchDkgTimedOut is a free log subscription operation binding the contract event 0x2852b3e178dd281713b041c3d90b4815bb55b7ec812931d1e8e8d8bb2ed72d3e. +// +// Solidity: event DkgTimedOut() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchDkgTimedOut(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryDkgTimedOut) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "DkgTimedOut") + 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(FrostWalletRegistryDkgTimedOut) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgTimedOut", 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 +} + +// ParseDkgTimedOut is a log parse operation binding the contract event 0x2852b3e178dd281713b041c3d90b4815bb55b7ec812931d1e8e8d8bb2ed72d3e. +// +// Solidity: event DkgTimedOut() +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseDkgTimedOut(log types.Log) (*FrostWalletRegistryDkgTimedOut, error) { + event := new(FrostWalletRegistryDkgTimedOut) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "DkgTimedOut", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryGasParametersUpdatedIterator is returned from FilterGasParametersUpdated and is used to iterate over the raw logs and unpacked data for GasParametersUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryGasParametersUpdatedIterator struct { + Event *FrostWalletRegistryGasParametersUpdated // 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 *FrostWalletRegistryGasParametersUpdatedIterator) 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(FrostWalletRegistryGasParametersUpdated) + 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(FrostWalletRegistryGasParametersUpdated) + 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 *FrostWalletRegistryGasParametersUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryGasParametersUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryGasParametersUpdated represents a GasParametersUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryGasParametersUpdated struct { + DkgResultSubmissionGas *big.Int + DkgResultApprovalGasOffset *big.Int + NotifyOperatorInactivityGasOffset *big.Int + NotifySeedTimeoutGasOffset *big.Int + NotifyDkgTimeoutNegativeGasOffset *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterGasParametersUpdated is a free log retrieval operation binding the contract event 0x8a3e64fa6013a36bccca7362e8826b11ba41e57fb60f55309c0ca48904dad082. +// +// Solidity: event GasParametersUpdated(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterGasParametersUpdated(opts *bind.FilterOpts) (*FrostWalletRegistryGasParametersUpdatedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "GasParametersUpdated") + if err != nil { + return nil, err + } + return &FrostWalletRegistryGasParametersUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "GasParametersUpdated", logs: logs, sub: sub}, nil +} + +// WatchGasParametersUpdated is a free log subscription operation binding the contract event 0x8a3e64fa6013a36bccca7362e8826b11ba41e57fb60f55309c0ca48904dad082. +// +// Solidity: event GasParametersUpdated(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchGasParametersUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryGasParametersUpdated) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "GasParametersUpdated") + 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(FrostWalletRegistryGasParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "GasParametersUpdated", 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 +} + +// ParseGasParametersUpdated is a log parse operation binding the contract event 0x8a3e64fa6013a36bccca7362e8826b11ba41e57fb60f55309c0ca48904dad082. +// +// Solidity: event GasParametersUpdated(uint256 dkgResultSubmissionGas, uint256 dkgResultApprovalGasOffset, uint256 notifyOperatorInactivityGasOffset, uint256 notifySeedTimeoutGasOffset, uint256 notifyDkgTimeoutNegativeGasOffset) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseGasParametersUpdated(log types.Log) (*FrostWalletRegistryGasParametersUpdated, error) { + event := new(FrostWalletRegistryGasParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "GasParametersUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryGovernanceTransferredIterator is returned from FilterGovernanceTransferred and is used to iterate over the raw logs and unpacked data for GovernanceTransferred events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryGovernanceTransferredIterator struct { + Event *FrostWalletRegistryGovernanceTransferred // 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 *FrostWalletRegistryGovernanceTransferredIterator) 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(FrostWalletRegistryGovernanceTransferred) + 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(FrostWalletRegistryGovernanceTransferred) + 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 *FrostWalletRegistryGovernanceTransferredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryGovernanceTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryGovernanceTransferred represents a GovernanceTransferred event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryGovernanceTransferred struct { + OldGovernance common.Address + NewGovernance common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterGovernanceTransferred is a free log retrieval operation binding the contract event 0x5f56bee8cffbe9a78652a74a60705edede02af10b0bbb888ca44b79a0d42ce80. +// +// Solidity: event GovernanceTransferred(address oldGovernance, address newGovernance) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterGovernanceTransferred(opts *bind.FilterOpts) (*FrostWalletRegistryGovernanceTransferredIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "GovernanceTransferred") + if err != nil { + return nil, err + } + return &FrostWalletRegistryGovernanceTransferredIterator{contract: _FrostWalletRegistry.contract, event: "GovernanceTransferred", logs: logs, sub: sub}, nil +} + +// WatchGovernanceTransferred is a free log subscription operation binding the contract event 0x5f56bee8cffbe9a78652a74a60705edede02af10b0bbb888ca44b79a0d42ce80. +// +// Solidity: event GovernanceTransferred(address oldGovernance, address newGovernance) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchGovernanceTransferred(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryGovernanceTransferred) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "GovernanceTransferred") + 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(FrostWalletRegistryGovernanceTransferred) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "GovernanceTransferred", 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 +} + +// ParseGovernanceTransferred is a log parse operation binding the contract event 0x5f56bee8cffbe9a78652a74a60705edede02af10b0bbb888ca44b79a0d42ce80. +// +// Solidity: event GovernanceTransferred(address oldGovernance, address newGovernance) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseGovernanceTransferred(log types.Log) (*FrostWalletRegistryGovernanceTransferred, error) { + event := new(FrostWalletRegistryGovernanceTransferred) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "GovernanceTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryInactivityClaimedIterator is returned from FilterInactivityClaimed and is used to iterate over the raw logs and unpacked data for InactivityClaimed events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryInactivityClaimedIterator struct { + Event *FrostWalletRegistryInactivityClaimed // 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 *FrostWalletRegistryInactivityClaimedIterator) 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(FrostWalletRegistryInactivityClaimed) + 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(FrostWalletRegistryInactivityClaimed) + 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 *FrostWalletRegistryInactivityClaimedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryInactivityClaimedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryInactivityClaimed represents a InactivityClaimed event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryInactivityClaimed struct { + WalletID [32]byte + Nonce *big.Int + Notifier common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInactivityClaimed is a free log retrieval operation binding the contract event 0x326e1ff7c130ed708307116f79cf7dbca649503e7082e5e35a19ceeee1523b39. +// +// Solidity: event InactivityClaimed(bytes32 indexed walletID, uint256 nonce, address notifier) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterInactivityClaimed(opts *bind.FilterOpts, walletID [][32]byte) (*FrostWalletRegistryInactivityClaimedIterator, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "InactivityClaimed", walletIDRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryInactivityClaimedIterator{contract: _FrostWalletRegistry.contract, event: "InactivityClaimed", logs: logs, sub: sub}, nil +} + +// WatchInactivityClaimed is a free log subscription operation binding the contract event 0x326e1ff7c130ed708307116f79cf7dbca649503e7082e5e35a19ceeee1523b39. +// +// Solidity: event InactivityClaimed(bytes32 indexed walletID, uint256 nonce, address notifier) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchInactivityClaimed(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryInactivityClaimed, walletID [][32]byte) (event.Subscription, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "InactivityClaimed", walletIDRule) + 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(FrostWalletRegistryInactivityClaimed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "InactivityClaimed", 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 +} + +// ParseInactivityClaimed is a log parse operation binding the contract event 0x326e1ff7c130ed708307116f79cf7dbca649503e7082e5e35a19ceeee1523b39. +// +// Solidity: event InactivityClaimed(bytes32 indexed walletID, uint256 nonce, address notifier) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseInactivityClaimed(log types.Log) (*FrostWalletRegistryInactivityClaimed, error) { + event := new(FrostWalletRegistryInactivityClaimed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "InactivityClaimed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryInitializedIterator is returned from FilterInitialized and is used to iterate over the raw logs and unpacked data for Initialized events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryInitializedIterator struct { + Event *FrostWalletRegistryInitialized // 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 *FrostWalletRegistryInitializedIterator) 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(FrostWalletRegistryInitialized) + 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(FrostWalletRegistryInitialized) + 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 *FrostWalletRegistryInitializedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryInitializedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryInitialized represents a Initialized event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryInitialized struct { + Version uint8 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInitialized is a free log retrieval operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterInitialized(opts *bind.FilterOpts) (*FrostWalletRegistryInitializedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "Initialized") + if err != nil { + return nil, err + } + return &FrostWalletRegistryInitializedIterator{contract: _FrostWalletRegistry.contract, event: "Initialized", logs: logs, sub: sub}, nil +} + +// WatchInitialized is a free log subscription operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchInitialized(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryInitialized) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "Initialized") + 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(FrostWalletRegistryInitialized) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "Initialized", 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 +} + +// ParseInitialized is a log parse operation binding the contract event 0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498. +// +// Solidity: event Initialized(uint8 version) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseInitialized(log types.Log) (*FrostWalletRegistryInitialized, error) { + event := new(FrostWalletRegistryInitialized) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "Initialized", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailedIterator is returned from FilterInvoluntaryAuthorizationDecreaseFailed and is used to iterate over the raw logs and unpacked data for InvoluntaryAuthorizationDecreaseFailed events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailedIterator struct { + Event *FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed // 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 *FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailedIterator) 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(FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed) + 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(FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed) + 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 *FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed represents a InvoluntaryAuthorizationDecreaseFailed event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed struct { + StakingProvider common.Address + Operator common.Address + FromAmount *big.Int + ToAmount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterInvoluntaryAuthorizationDecreaseFailed is a free log retrieval operation binding the contract event 0x1b09380d63e78fd72c1d79a805a7e2dfadf02b22418e24bebff51376b7df33b0. +// +// Solidity: event InvoluntaryAuthorizationDecreaseFailed(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterInvoluntaryAuthorizationDecreaseFailed(opts *bind.FilterOpts, stakingProvider []common.Address, operator []common.Address) (*FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailedIterator, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "InvoluntaryAuthorizationDecreaseFailed", stakingProviderRule, operatorRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailedIterator{contract: _FrostWalletRegistry.contract, event: "InvoluntaryAuthorizationDecreaseFailed", logs: logs, sub: sub}, nil +} + +// WatchInvoluntaryAuthorizationDecreaseFailed is a free log subscription operation binding the contract event 0x1b09380d63e78fd72c1d79a805a7e2dfadf02b22418e24bebff51376b7df33b0. +// +// Solidity: event InvoluntaryAuthorizationDecreaseFailed(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchInvoluntaryAuthorizationDecreaseFailed(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed, stakingProvider []common.Address, operator []common.Address) (event.Subscription, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "InvoluntaryAuthorizationDecreaseFailed", stakingProviderRule, operatorRule) + 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(FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "InvoluntaryAuthorizationDecreaseFailed", 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 +} + +// ParseInvoluntaryAuthorizationDecreaseFailed is a log parse operation binding the contract event 0x1b09380d63e78fd72c1d79a805a7e2dfadf02b22418e24bebff51376b7df33b0. +// +// Solidity: event InvoluntaryAuthorizationDecreaseFailed(address indexed stakingProvider, address indexed operator, uint96 fromAmount, uint96 toAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseInvoluntaryAuthorizationDecreaseFailed(log types.Log) (*FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed, error) { + event := new(FrostWalletRegistryInvoluntaryAuthorizationDecreaseFailed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "InvoluntaryAuthorizationDecreaseFailed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryLifecycleOwnerUpdatedIterator is returned from FilterLifecycleOwnerUpdated and is used to iterate over the raw logs and unpacked data for LifecycleOwnerUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryLifecycleOwnerUpdatedIterator struct { + Event *FrostWalletRegistryLifecycleOwnerUpdated // 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 *FrostWalletRegistryLifecycleOwnerUpdatedIterator) 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(FrostWalletRegistryLifecycleOwnerUpdated) + 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(FrostWalletRegistryLifecycleOwnerUpdated) + 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 *FrostWalletRegistryLifecycleOwnerUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryLifecycleOwnerUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryLifecycleOwnerUpdated represents a LifecycleOwnerUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryLifecycleOwnerUpdated struct { + LifecycleOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterLifecycleOwnerUpdated is a free log retrieval operation binding the contract event 0xc41594e25066d174fb0130f0ddd858b71b9a4f035b2f07d903a4385337c93382. +// +// Solidity: event LifecycleOwnerUpdated(address lifecycleOwner) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterLifecycleOwnerUpdated(opts *bind.FilterOpts) (*FrostWalletRegistryLifecycleOwnerUpdatedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "LifecycleOwnerUpdated") + if err != nil { + return nil, err + } + return &FrostWalletRegistryLifecycleOwnerUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "LifecycleOwnerUpdated", logs: logs, sub: sub}, nil +} + +// WatchLifecycleOwnerUpdated is a free log subscription operation binding the contract event 0xc41594e25066d174fb0130f0ddd858b71b9a4f035b2f07d903a4385337c93382. +// +// Solidity: event LifecycleOwnerUpdated(address lifecycleOwner) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchLifecycleOwnerUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryLifecycleOwnerUpdated) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "LifecycleOwnerUpdated") + 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(FrostWalletRegistryLifecycleOwnerUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "LifecycleOwnerUpdated", 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 +} + +// ParseLifecycleOwnerUpdated is a log parse operation binding the contract event 0xc41594e25066d174fb0130f0ddd858b71b9a4f035b2f07d903a4385337c93382. +// +// Solidity: event LifecycleOwnerUpdated(address lifecycleOwner) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseLifecycleOwnerUpdated(log types.Log) (*FrostWalletRegistryLifecycleOwnerUpdated, error) { + event := new(FrostWalletRegistryLifecycleOwnerUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "LifecycleOwnerUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryOperatorJoinedSortitionPoolIterator is returned from FilterOperatorJoinedSortitionPool and is used to iterate over the raw logs and unpacked data for OperatorJoinedSortitionPool events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryOperatorJoinedSortitionPoolIterator struct { + Event *FrostWalletRegistryOperatorJoinedSortitionPool // 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 *FrostWalletRegistryOperatorJoinedSortitionPoolIterator) 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(FrostWalletRegistryOperatorJoinedSortitionPool) + 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(FrostWalletRegistryOperatorJoinedSortitionPool) + 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 *FrostWalletRegistryOperatorJoinedSortitionPoolIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryOperatorJoinedSortitionPoolIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryOperatorJoinedSortitionPool represents a OperatorJoinedSortitionPool event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryOperatorJoinedSortitionPool struct { + StakingProvider common.Address + Operator common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOperatorJoinedSortitionPool is a free log retrieval operation binding the contract event 0x5075aaa89894a888eb2cac81a27320c60855febb0cf1706b66bdc754e640d433. +// +// Solidity: event OperatorJoinedSortitionPool(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterOperatorJoinedSortitionPool(opts *bind.FilterOpts, stakingProvider []common.Address, operator []common.Address) (*FrostWalletRegistryOperatorJoinedSortitionPoolIterator, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "OperatorJoinedSortitionPool", stakingProviderRule, operatorRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryOperatorJoinedSortitionPoolIterator{contract: _FrostWalletRegistry.contract, event: "OperatorJoinedSortitionPool", logs: logs, sub: sub}, nil +} + +// WatchOperatorJoinedSortitionPool is a free log subscription operation binding the contract event 0x5075aaa89894a888eb2cac81a27320c60855febb0cf1706b66bdc754e640d433. +// +// Solidity: event OperatorJoinedSortitionPool(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchOperatorJoinedSortitionPool(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryOperatorJoinedSortitionPool, stakingProvider []common.Address, operator []common.Address) (event.Subscription, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "OperatorJoinedSortitionPool", stakingProviderRule, operatorRule) + 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(FrostWalletRegistryOperatorJoinedSortitionPool) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "OperatorJoinedSortitionPool", 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 +} + +// ParseOperatorJoinedSortitionPool is a log parse operation binding the contract event 0x5075aaa89894a888eb2cac81a27320c60855febb0cf1706b66bdc754e640d433. +// +// Solidity: event OperatorJoinedSortitionPool(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseOperatorJoinedSortitionPool(log types.Log) (*FrostWalletRegistryOperatorJoinedSortitionPool, error) { + event := new(FrostWalletRegistryOperatorJoinedSortitionPool) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "OperatorJoinedSortitionPool", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryOperatorRegisteredIterator is returned from FilterOperatorRegistered and is used to iterate over the raw logs and unpacked data for OperatorRegistered events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryOperatorRegisteredIterator struct { + Event *FrostWalletRegistryOperatorRegistered // 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 *FrostWalletRegistryOperatorRegisteredIterator) 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(FrostWalletRegistryOperatorRegistered) + 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(FrostWalletRegistryOperatorRegistered) + 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 *FrostWalletRegistryOperatorRegisteredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryOperatorRegisteredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryOperatorRegistered represents a OperatorRegistered event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryOperatorRegistered struct { + StakingProvider common.Address + Operator common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOperatorRegistered is a free log retrieval operation binding the contract event 0xa453db612af59e5521d6ab9284dc3e2d06af286eb1b1b7b771fce4716c19f2c1. +// +// Solidity: event OperatorRegistered(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterOperatorRegistered(opts *bind.FilterOpts, stakingProvider []common.Address, operator []common.Address) (*FrostWalletRegistryOperatorRegisteredIterator, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "OperatorRegistered", stakingProviderRule, operatorRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryOperatorRegisteredIterator{contract: _FrostWalletRegistry.contract, event: "OperatorRegistered", logs: logs, sub: sub}, nil +} + +// WatchOperatorRegistered is a free log subscription operation binding the contract event 0xa453db612af59e5521d6ab9284dc3e2d06af286eb1b1b7b771fce4716c19f2c1. +// +// Solidity: event OperatorRegistered(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchOperatorRegistered(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryOperatorRegistered, stakingProvider []common.Address, operator []common.Address) (event.Subscription, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "OperatorRegistered", stakingProviderRule, operatorRule) + 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(FrostWalletRegistryOperatorRegistered) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "OperatorRegistered", 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 +} + +// ParseOperatorRegistered is a log parse operation binding the contract event 0xa453db612af59e5521d6ab9284dc3e2d06af286eb1b1b7b771fce4716c19f2c1. +// +// Solidity: event OperatorRegistered(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseOperatorRegistered(log types.Log) (*FrostWalletRegistryOperatorRegistered, error) { + event := new(FrostWalletRegistryOperatorRegistered) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "OperatorRegistered", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryOperatorStatusUpdatedIterator is returned from FilterOperatorStatusUpdated and is used to iterate over the raw logs and unpacked data for OperatorStatusUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryOperatorStatusUpdatedIterator struct { + Event *FrostWalletRegistryOperatorStatusUpdated // 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 *FrostWalletRegistryOperatorStatusUpdatedIterator) 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(FrostWalletRegistryOperatorStatusUpdated) + 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(FrostWalletRegistryOperatorStatusUpdated) + 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 *FrostWalletRegistryOperatorStatusUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryOperatorStatusUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryOperatorStatusUpdated represents a OperatorStatusUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryOperatorStatusUpdated struct { + StakingProvider common.Address + Operator common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterOperatorStatusUpdated is a free log retrieval operation binding the contract event 0x1231fe5ee649a593b524a494cd53146a196380a872115a0d0fe16c0735afdf26. +// +// Solidity: event OperatorStatusUpdated(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterOperatorStatusUpdated(opts *bind.FilterOpts, stakingProvider []common.Address, operator []common.Address) (*FrostWalletRegistryOperatorStatusUpdatedIterator, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "OperatorStatusUpdated", stakingProviderRule, operatorRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryOperatorStatusUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "OperatorStatusUpdated", logs: logs, sub: sub}, nil +} + +// WatchOperatorStatusUpdated is a free log subscription operation binding the contract event 0x1231fe5ee649a593b524a494cd53146a196380a872115a0d0fe16c0735afdf26. +// +// Solidity: event OperatorStatusUpdated(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchOperatorStatusUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryOperatorStatusUpdated, stakingProvider []common.Address, operator []common.Address) (event.Subscription, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + var operatorRule []interface{} + for _, operatorItem := range operator { + operatorRule = append(operatorRule, operatorItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "OperatorStatusUpdated", stakingProviderRule, operatorRule) + 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(FrostWalletRegistryOperatorStatusUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "OperatorStatusUpdated", 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 +} + +// ParseOperatorStatusUpdated is a log parse operation binding the contract event 0x1231fe5ee649a593b524a494cd53146a196380a872115a0d0fe16c0735afdf26. +// +// Solidity: event OperatorStatusUpdated(address indexed stakingProvider, address indexed operator) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseOperatorStatusUpdated(log types.Log) (*FrostWalletRegistryOperatorStatusUpdated, error) { + event := new(FrostWalletRegistryOperatorStatusUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "OperatorStatusUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryRandomBeaconUpgradedIterator is returned from FilterRandomBeaconUpgraded and is used to iterate over the raw logs and unpacked data for RandomBeaconUpgraded events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryRandomBeaconUpgradedIterator struct { + Event *FrostWalletRegistryRandomBeaconUpgraded // 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 *FrostWalletRegistryRandomBeaconUpgradedIterator) 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(FrostWalletRegistryRandomBeaconUpgraded) + 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(FrostWalletRegistryRandomBeaconUpgraded) + 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 *FrostWalletRegistryRandomBeaconUpgradedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryRandomBeaconUpgradedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryRandomBeaconUpgraded represents a RandomBeaconUpgraded event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryRandomBeaconUpgraded struct { + RandomBeacon common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRandomBeaconUpgraded is a free log retrieval operation binding the contract event 0x2b34e21b6daa8fcf8cba1c3ed709cbed2b0231d5fb60e9ccd8c2e75a5674bcb3. +// +// Solidity: event RandomBeaconUpgraded(address randomBeacon) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterRandomBeaconUpgraded(opts *bind.FilterOpts) (*FrostWalletRegistryRandomBeaconUpgradedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "RandomBeaconUpgraded") + if err != nil { + return nil, err + } + return &FrostWalletRegistryRandomBeaconUpgradedIterator{contract: _FrostWalletRegistry.contract, event: "RandomBeaconUpgraded", logs: logs, sub: sub}, nil +} + +// WatchRandomBeaconUpgraded is a free log subscription operation binding the contract event 0x2b34e21b6daa8fcf8cba1c3ed709cbed2b0231d5fb60e9ccd8c2e75a5674bcb3. +// +// Solidity: event RandomBeaconUpgraded(address randomBeacon) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchRandomBeaconUpgraded(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryRandomBeaconUpgraded) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "RandomBeaconUpgraded") + 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(FrostWalletRegistryRandomBeaconUpgraded) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "RandomBeaconUpgraded", 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 +} + +// ParseRandomBeaconUpgraded is a log parse operation binding the contract event 0x2b34e21b6daa8fcf8cba1c3ed709cbed2b0231d5fb60e9ccd8c2e75a5674bcb3. +// +// Solidity: event RandomBeaconUpgraded(address randomBeacon) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseRandomBeaconUpgraded(log types.Log) (*FrostWalletRegistryRandomBeaconUpgraded, error) { + event := new(FrostWalletRegistryRandomBeaconUpgraded) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "RandomBeaconUpgraded", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryReimbursementPoolUpdatedIterator is returned from FilterReimbursementPoolUpdated and is used to iterate over the raw logs and unpacked data for ReimbursementPoolUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryReimbursementPoolUpdatedIterator struct { + Event *FrostWalletRegistryReimbursementPoolUpdated // 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 *FrostWalletRegistryReimbursementPoolUpdatedIterator) 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(FrostWalletRegistryReimbursementPoolUpdated) + 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(FrostWalletRegistryReimbursementPoolUpdated) + 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 *FrostWalletRegistryReimbursementPoolUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryReimbursementPoolUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryReimbursementPoolUpdated represents a ReimbursementPoolUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryReimbursementPoolUpdated struct { + NewReimbursementPool common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterReimbursementPoolUpdated is a free log retrieval operation binding the contract event 0x0e2d2343d31b085b7c4e56d1c8a6ec79f7ab07460386f1c9a1756239fe2533ac. +// +// Solidity: event ReimbursementPoolUpdated(address newReimbursementPool) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterReimbursementPoolUpdated(opts *bind.FilterOpts) (*FrostWalletRegistryReimbursementPoolUpdatedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "ReimbursementPoolUpdated") + if err != nil { + return nil, err + } + return &FrostWalletRegistryReimbursementPoolUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "ReimbursementPoolUpdated", logs: logs, sub: sub}, nil +} + +// WatchReimbursementPoolUpdated is a free log subscription operation binding the contract event 0x0e2d2343d31b085b7c4e56d1c8a6ec79f7ab07460386f1c9a1756239fe2533ac. +// +// Solidity: event ReimbursementPoolUpdated(address newReimbursementPool) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchReimbursementPoolUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryReimbursementPoolUpdated) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "ReimbursementPoolUpdated") + 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(FrostWalletRegistryReimbursementPoolUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "ReimbursementPoolUpdated", 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 +} + +// ParseReimbursementPoolUpdated is a log parse operation binding the contract event 0x0e2d2343d31b085b7c4e56d1c8a6ec79f7ab07460386f1c9a1756239fe2533ac. +// +// Solidity: event ReimbursementPoolUpdated(address newReimbursementPool) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseReimbursementPoolUpdated(log types.Log) (*FrostWalletRegistryReimbursementPoolUpdated, error) { + event := new(FrostWalletRegistryReimbursementPoolUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "ReimbursementPoolUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryRewardParametersUpdatedIterator is returned from FilterRewardParametersUpdated and is used to iterate over the raw logs and unpacked data for RewardParametersUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryRewardParametersUpdatedIterator struct { + Event *FrostWalletRegistryRewardParametersUpdated // 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 *FrostWalletRegistryRewardParametersUpdatedIterator) 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(FrostWalletRegistryRewardParametersUpdated) + 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(FrostWalletRegistryRewardParametersUpdated) + 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 *FrostWalletRegistryRewardParametersUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryRewardParametersUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryRewardParametersUpdated represents a RewardParametersUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryRewardParametersUpdated struct { + MaliciousDkgResultNotificationRewardMultiplier *big.Int + SortitionPoolRewardsBanDuration *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRewardParametersUpdated is a free log retrieval operation binding the contract event 0xf3a6ee10a78fb7d212e87d9be970fb16bd7324e9dc9c38d21cd7ecde781a1d2a. +// +// Solidity: event RewardParametersUpdated(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterRewardParametersUpdated(opts *bind.FilterOpts) (*FrostWalletRegistryRewardParametersUpdatedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "RewardParametersUpdated") + if err != nil { + return nil, err + } + return &FrostWalletRegistryRewardParametersUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "RewardParametersUpdated", logs: logs, sub: sub}, nil +} + +// WatchRewardParametersUpdated is a free log subscription operation binding the contract event 0xf3a6ee10a78fb7d212e87d9be970fb16bd7324e9dc9c38d21cd7ecde781a1d2a. +// +// Solidity: event RewardParametersUpdated(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchRewardParametersUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryRewardParametersUpdated) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "RewardParametersUpdated") + 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(FrostWalletRegistryRewardParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "RewardParametersUpdated", 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 +} + +// ParseRewardParametersUpdated is a log parse operation binding the contract event 0xf3a6ee10a78fb7d212e87d9be970fb16bd7324e9dc9c38d21cd7ecde781a1d2a. +// +// Solidity: event RewardParametersUpdated(uint256 maliciousDkgResultNotificationRewardMultiplier, uint256 sortitionPoolRewardsBanDuration) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseRewardParametersUpdated(log types.Log) (*FrostWalletRegistryRewardParametersUpdated, error) { + event := new(FrostWalletRegistryRewardParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "RewardParametersUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryRewardsWithdrawnIterator is returned from FilterRewardsWithdrawn and is used to iterate over the raw logs and unpacked data for RewardsWithdrawn events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryRewardsWithdrawnIterator struct { + Event *FrostWalletRegistryRewardsWithdrawn // 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 *FrostWalletRegistryRewardsWithdrawnIterator) 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(FrostWalletRegistryRewardsWithdrawn) + 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(FrostWalletRegistryRewardsWithdrawn) + 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 *FrostWalletRegistryRewardsWithdrawnIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryRewardsWithdrawnIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryRewardsWithdrawn represents a RewardsWithdrawn event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryRewardsWithdrawn struct { + StakingProvider common.Address + Amount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterRewardsWithdrawn is a free log retrieval operation binding the contract event 0x38532b6dea69d7266fa923c7813d190be37625f2454ddfa3d93c45c79482e3fd. +// +// Solidity: event RewardsWithdrawn(address indexed stakingProvider, uint96 amount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterRewardsWithdrawn(opts *bind.FilterOpts, stakingProvider []common.Address) (*FrostWalletRegistryRewardsWithdrawnIterator, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "RewardsWithdrawn", stakingProviderRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryRewardsWithdrawnIterator{contract: _FrostWalletRegistry.contract, event: "RewardsWithdrawn", logs: logs, sub: sub}, nil +} + +// WatchRewardsWithdrawn is a free log subscription operation binding the contract event 0x38532b6dea69d7266fa923c7813d190be37625f2454ddfa3d93c45c79482e3fd. +// +// Solidity: event RewardsWithdrawn(address indexed stakingProvider, uint96 amount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchRewardsWithdrawn(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryRewardsWithdrawn, stakingProvider []common.Address) (event.Subscription, error) { + + var stakingProviderRule []interface{} + for _, stakingProviderItem := range stakingProvider { + stakingProviderRule = append(stakingProviderRule, stakingProviderItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "RewardsWithdrawn", stakingProviderRule) + 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(FrostWalletRegistryRewardsWithdrawn) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "RewardsWithdrawn", 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 +} + +// ParseRewardsWithdrawn is a log parse operation binding the contract event 0x38532b6dea69d7266fa923c7813d190be37625f2454ddfa3d93c45c79482e3fd. +// +// Solidity: event RewardsWithdrawn(address indexed stakingProvider, uint96 amount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseRewardsWithdrawn(log types.Log) (*FrostWalletRegistryRewardsWithdrawn, error) { + event := new(FrostWalletRegistryRewardsWithdrawn) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "RewardsWithdrawn", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistrySlashingParametersUpdatedIterator is returned from FilterSlashingParametersUpdated and is used to iterate over the raw logs and unpacked data for SlashingParametersUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistrySlashingParametersUpdatedIterator struct { + Event *FrostWalletRegistrySlashingParametersUpdated // 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 *FrostWalletRegistrySlashingParametersUpdatedIterator) 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(FrostWalletRegistrySlashingParametersUpdated) + 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(FrostWalletRegistrySlashingParametersUpdated) + 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 *FrostWalletRegistrySlashingParametersUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistrySlashingParametersUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistrySlashingParametersUpdated represents a SlashingParametersUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistrySlashingParametersUpdated struct { + MaliciousDkgResultSlashingAmount *big.Int + Raw types.Log // Blockchain specific contextual infos +} + +// FilterSlashingParametersUpdated is a free log retrieval operation binding the contract event 0xe132b87eb6644ee4d4c3c32744f7e1c3906335a2d4f99330767bf573909c7d84. +// +// Solidity: event SlashingParametersUpdated(uint256 maliciousDkgResultSlashingAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterSlashingParametersUpdated(opts *bind.FilterOpts) (*FrostWalletRegistrySlashingParametersUpdatedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "SlashingParametersUpdated") + if err != nil { + return nil, err + } + return &FrostWalletRegistrySlashingParametersUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "SlashingParametersUpdated", logs: logs, sub: sub}, nil +} + +// WatchSlashingParametersUpdated is a free log subscription operation binding the contract event 0xe132b87eb6644ee4d4c3c32744f7e1c3906335a2d4f99330767bf573909c7d84. +// +// Solidity: event SlashingParametersUpdated(uint256 maliciousDkgResultSlashingAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchSlashingParametersUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistrySlashingParametersUpdated) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "SlashingParametersUpdated") + 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(FrostWalletRegistrySlashingParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "SlashingParametersUpdated", 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 +} + +// ParseSlashingParametersUpdated is a log parse operation binding the contract event 0xe132b87eb6644ee4d4c3c32744f7e1c3906335a2d4f99330767bf573909c7d84. +// +// Solidity: event SlashingParametersUpdated(uint256 maliciousDkgResultSlashingAmount) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseSlashingParametersUpdated(log types.Log) (*FrostWalletRegistrySlashingParametersUpdated, error) { + event := new(FrostWalletRegistrySlashingParametersUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "SlashingParametersUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryWalletClosedIterator is returned from FilterWalletClosed and is used to iterate over the raw logs and unpacked data for WalletClosed events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryWalletClosedIterator struct { + Event *FrostWalletRegistryWalletClosed // 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 *FrostWalletRegistryWalletClosedIterator) 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(FrostWalletRegistryWalletClosed) + 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(FrostWalletRegistryWalletClosed) + 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 *FrostWalletRegistryWalletClosedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryWalletClosedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryWalletClosed represents a WalletClosed event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryWalletClosed struct { + WalletID [32]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWalletClosed is a free log retrieval operation binding the contract event 0xa6ae4af610b8ada39d3675190ead27a5552631a8e33f53e4e37dbb082f11a73e. +// +// Solidity: event WalletClosed(bytes32 indexed walletID) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterWalletClosed(opts *bind.FilterOpts, walletID [][32]byte) (*FrostWalletRegistryWalletClosedIterator, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "WalletClosed", walletIDRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryWalletClosedIterator{contract: _FrostWalletRegistry.contract, event: "WalletClosed", logs: logs, sub: sub}, nil +} + +// WatchWalletClosed is a free log subscription operation binding the contract event 0xa6ae4af610b8ada39d3675190ead27a5552631a8e33f53e4e37dbb082f11a73e. +// +// Solidity: event WalletClosed(bytes32 indexed walletID) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchWalletClosed(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryWalletClosed, walletID [][32]byte) (event.Subscription, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "WalletClosed", walletIDRule) + 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(FrostWalletRegistryWalletClosed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "WalletClosed", 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 +} + +// ParseWalletClosed is a log parse operation binding the contract event 0xa6ae4af610b8ada39d3675190ead27a5552631a8e33f53e4e37dbb082f11a73e. +// +// Solidity: event WalletClosed(bytes32 indexed walletID) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseWalletClosed(log types.Log) (*FrostWalletRegistryWalletClosed, error) { + event := new(FrostWalletRegistryWalletClosed) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "WalletClosed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryWalletCreatedIterator is returned from FilterWalletCreated and is used to iterate over the raw logs and unpacked data for WalletCreated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryWalletCreatedIterator struct { + Event *FrostWalletRegistryWalletCreated // 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 *FrostWalletRegistryWalletCreatedIterator) 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(FrostWalletRegistryWalletCreated) + 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(FrostWalletRegistryWalletCreated) + 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 *FrostWalletRegistryWalletCreatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryWalletCreatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryWalletCreated represents a WalletCreated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryWalletCreated struct { + WalletID [32]byte + DkgResultHash [32]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWalletCreated is a free log retrieval operation binding the contract event 0xbe8f27cef1f3d94120c9c547c3614f5b992fdb0c0a497cc920fde06546291ab4. +// +// Solidity: event WalletCreated(bytes32 indexed walletID, bytes32 indexed dkgResultHash) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterWalletCreated(opts *bind.FilterOpts, walletID [][32]byte, dkgResultHash [][32]byte) (*FrostWalletRegistryWalletCreatedIterator, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + var dkgResultHashRule []interface{} + for _, dkgResultHashItem := range dkgResultHash { + dkgResultHashRule = append(dkgResultHashRule, dkgResultHashItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "WalletCreated", walletIDRule, dkgResultHashRule) + if err != nil { + return nil, err + } + return &FrostWalletRegistryWalletCreatedIterator{contract: _FrostWalletRegistry.contract, event: "WalletCreated", logs: logs, sub: sub}, nil +} + +// WatchWalletCreated is a free log subscription operation binding the contract event 0xbe8f27cef1f3d94120c9c547c3614f5b992fdb0c0a497cc920fde06546291ab4. +// +// Solidity: event WalletCreated(bytes32 indexed walletID, bytes32 indexed dkgResultHash) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchWalletCreated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryWalletCreated, walletID [][32]byte, dkgResultHash [][32]byte) (event.Subscription, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + var dkgResultHashRule []interface{} + for _, dkgResultHashItem := range dkgResultHash { + dkgResultHashRule = append(dkgResultHashRule, dkgResultHashItem) + } + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "WalletCreated", walletIDRule, dkgResultHashRule) + 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(FrostWalletRegistryWalletCreated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "WalletCreated", 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 +} + +// ParseWalletCreated is a log parse operation binding the contract event 0xbe8f27cef1f3d94120c9c547c3614f5b992fdb0c0a497cc920fde06546291ab4. +// +// Solidity: event WalletCreated(bytes32 indexed walletID, bytes32 indexed dkgResultHash) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseWalletCreated(log types.Log) (*FrostWalletRegistryWalletCreated, error) { + event := new(FrostWalletRegistryWalletCreated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "WalletCreated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// FrostWalletRegistryWalletOwnerUpdatedIterator is returned from FilterWalletOwnerUpdated and is used to iterate over the raw logs and unpacked data for WalletOwnerUpdated events raised by the FrostWalletRegistry contract. +type FrostWalletRegistryWalletOwnerUpdatedIterator struct { + Event *FrostWalletRegistryWalletOwnerUpdated // 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 *FrostWalletRegistryWalletOwnerUpdatedIterator) 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(FrostWalletRegistryWalletOwnerUpdated) + 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(FrostWalletRegistryWalletOwnerUpdated) + 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 *FrostWalletRegistryWalletOwnerUpdatedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *FrostWalletRegistryWalletOwnerUpdatedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// FrostWalletRegistryWalletOwnerUpdated represents a WalletOwnerUpdated event raised by the FrostWalletRegistry contract. +type FrostWalletRegistryWalletOwnerUpdated struct { + WalletOwner common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterWalletOwnerUpdated is a free log retrieval operation binding the contract event 0xa1993af5a189ba5ad4155263c920cfee33ce0593a8eb231a13bb3ce6f39459e3. +// +// Solidity: event WalletOwnerUpdated(address walletOwner) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) FilterWalletOwnerUpdated(opts *bind.FilterOpts) (*FrostWalletRegistryWalletOwnerUpdatedIterator, error) { + + logs, sub, err := _FrostWalletRegistry.contract.FilterLogs(opts, "WalletOwnerUpdated") + if err != nil { + return nil, err + } + return &FrostWalletRegistryWalletOwnerUpdatedIterator{contract: _FrostWalletRegistry.contract, event: "WalletOwnerUpdated", logs: logs, sub: sub}, nil +} + +// WatchWalletOwnerUpdated is a free log subscription operation binding the contract event 0xa1993af5a189ba5ad4155263c920cfee33ce0593a8eb231a13bb3ce6f39459e3. +// +// Solidity: event WalletOwnerUpdated(address walletOwner) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) WatchWalletOwnerUpdated(opts *bind.WatchOpts, sink chan<- *FrostWalletRegistryWalletOwnerUpdated) (event.Subscription, error) { + + logs, sub, err := _FrostWalletRegistry.contract.WatchLogs(opts, "WalletOwnerUpdated") + 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(FrostWalletRegistryWalletOwnerUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "WalletOwnerUpdated", 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 +} + +// ParseWalletOwnerUpdated is a log parse operation binding the contract event 0xa1993af5a189ba5ad4155263c920cfee33ce0593a8eb231a13bb3ce6f39459e3. +// +// Solidity: event WalletOwnerUpdated(address walletOwner) +func (_FrostWalletRegistry *FrostWalletRegistryFilterer) ParseWalletOwnerUpdated(log types.Log) (*FrostWalletRegistryWalletOwnerUpdated, error) { + event := new(FrostWalletRegistryWalletOwnerUpdated) + if err := _FrostWalletRegistry.contract.UnpackLog(event, "WalletOwnerUpdated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} diff --git a/pkg/chain/ethereum/frost/gen/gen.go b/pkg/chain/ethereum/frost/gen/gen.go new file mode 100644 index 0000000000..50c56d7157 --- /dev/null +++ b/pkg/chain/ethereum/frost/gen/gen.go @@ -0,0 +1,13 @@ +package gen + +var ( + // FrostWalletRegistryAddress is zero for development builds. Operators must + // configure the deployed registry address explicitly until the FROST + // registry artifact is published with network addresses. + FrostWalletRegistryAddress = "0x0000000000000000000000000000000000000000" + + // FrostDkgValidatorAddress is zero for development builds. It is optional + // for runtime challenge checks, which use FrostWalletRegistry.isDkgResultValid, + // but can be configured for pre-submit resultDigest sanity checks. + FrostDkgValidatorAddress = "0x0000000000000000000000000000000000000000" +) diff --git a/pkg/chain/ethereum/frost/gen/validatorabi/FrostDkgValidator.go b/pkg/chain/ethereum/frost/gen/validatorabi/FrostDkgValidator.go new file mode 100644 index 0000000000..f7a1a84792 --- /dev/null +++ b/pkg/chain/ethereum/frost/gen/validatorabi/FrostDkgValidator.go @@ -0,0 +1,598 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package validatorabi + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// FrostDkgResult is an auto generated low-level Go binding around an user-defined struct. +type FrostDkgResult struct { + SubmitterMemberIndex *big.Int + XOnlyOutputKey [32]byte + MisbehavedMembersIndices []uint8 + Signatures []byte + SigningMembersIndices []*big.Int + Members []uint32 + MembersHash [32]byte +} + +// FrostDkgValidatorDigestBinding is an auto generated low-level Go binding around an user-defined struct. +type FrostDkgValidatorDigestBinding struct { + Bridge common.Address + Registry common.Address +} + +// FrostDkgValidatorMetaData contains all meta data concerning the FrostDkgValidator contract. +var FrostDkgValidatorMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"contractSortitionPool\",\"name\":\"_sortitionPool\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"activeThreshold\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"groupSize\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"groupThreshold\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"publicKeyByteSize\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"result\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"seed\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"bridge\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"registry\",\"type\":\"address\"}],\"name\":\"resultDigest\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"digest\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"signatureByteSize\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"sortitionPool\",\"outputs\":[{\"internalType\":\"contractSortitionPool\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"result\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"seed\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"startBlock\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"bridge\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"registry\",\"type\":\"address\"}],\"name\":\"validate\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"isValid\",\"type\":\"bool\"},{\"internalType\":\"string\",\"name\":\"errorMsg\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"result\",\"type\":\"tuple\"}],\"name\":\"validateFields\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"isValid\",\"type\":\"bool\"},{\"internalType\":\"string\",\"name\":\"errorMsg\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"result\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"seed\",\"type\":\"uint256\"}],\"name\":\"validateGroupMembers\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"result\",\"type\":\"tuple\"}],\"name\":\"validateMembersHash\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"uint256\",\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"internalType\":\"uint8[]\",\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"internalType\":\"bytes\",\"name\":\"signatures\",\"type\":\"bytes\"},{\"internalType\":\"uint256[]\",\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"internalType\":\"uint32[]\",\"name\":\"members\",\"type\":\"uint32[]\"},{\"internalType\":\"bytes32\",\"name\":\"membersHash\",\"type\":\"bytes32\"}],\"internalType\":\"structFrostDkg.Result\",\"name\":\"result\",\"type\":\"tuple\"},{\"internalType\":\"uint256\",\"name\":\"seed\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"bridge\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"registry\",\"type\":\"address\"}],\"internalType\":\"structFrostDkgValidator.DigestBinding\",\"name\":\"binding\",\"type\":\"tuple\"}],\"name\":\"validateSignatures\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// FrostDkgValidatorABI is the input ABI used to generate the binding from. +// Deprecated: Use FrostDkgValidatorMetaData.ABI instead. +var FrostDkgValidatorABI = FrostDkgValidatorMetaData.ABI + +// FrostDkgValidator is an auto generated Go binding around an Ethereum contract. +type FrostDkgValidator struct { + FrostDkgValidatorCaller // Read-only binding to the contract + FrostDkgValidatorTransactor // Write-only binding to the contract + FrostDkgValidatorFilterer // Log filterer for contract events +} + +// FrostDkgValidatorCaller is an auto generated read-only Go binding around an Ethereum contract. +type FrostDkgValidatorCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FrostDkgValidatorTransactor is an auto generated write-only Go binding around an Ethereum contract. +type FrostDkgValidatorTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FrostDkgValidatorFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type FrostDkgValidatorFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FrostDkgValidatorSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type FrostDkgValidatorSession struct { + Contract *FrostDkgValidator // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FrostDkgValidatorCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type FrostDkgValidatorCallerSession struct { + Contract *FrostDkgValidatorCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// FrostDkgValidatorTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type FrostDkgValidatorTransactorSession struct { + Contract *FrostDkgValidatorTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FrostDkgValidatorRaw is an auto generated low-level Go binding around an Ethereum contract. +type FrostDkgValidatorRaw struct { + Contract *FrostDkgValidator // Generic contract binding to access the raw methods on +} + +// FrostDkgValidatorCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type FrostDkgValidatorCallerRaw struct { + Contract *FrostDkgValidatorCaller // Generic read-only contract binding to access the raw methods on +} + +// FrostDkgValidatorTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type FrostDkgValidatorTransactorRaw struct { + Contract *FrostDkgValidatorTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewFrostDkgValidator creates a new instance of FrostDkgValidator, bound to a specific deployed contract. +func NewFrostDkgValidator(address common.Address, backend bind.ContractBackend) (*FrostDkgValidator, error) { + contract, err := bindFrostDkgValidator(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &FrostDkgValidator{FrostDkgValidatorCaller: FrostDkgValidatorCaller{contract: contract}, FrostDkgValidatorTransactor: FrostDkgValidatorTransactor{contract: contract}, FrostDkgValidatorFilterer: FrostDkgValidatorFilterer{contract: contract}}, nil +} + +// NewFrostDkgValidatorCaller creates a new read-only instance of FrostDkgValidator, bound to a specific deployed contract. +func NewFrostDkgValidatorCaller(address common.Address, caller bind.ContractCaller) (*FrostDkgValidatorCaller, error) { + contract, err := bindFrostDkgValidator(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &FrostDkgValidatorCaller{contract: contract}, nil +} + +// NewFrostDkgValidatorTransactor creates a new write-only instance of FrostDkgValidator, bound to a specific deployed contract. +func NewFrostDkgValidatorTransactor(address common.Address, transactor bind.ContractTransactor) (*FrostDkgValidatorTransactor, error) { + contract, err := bindFrostDkgValidator(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &FrostDkgValidatorTransactor{contract: contract}, nil +} + +// NewFrostDkgValidatorFilterer creates a new log filterer instance of FrostDkgValidator, bound to a specific deployed contract. +func NewFrostDkgValidatorFilterer(address common.Address, filterer bind.ContractFilterer) (*FrostDkgValidatorFilterer, error) { + contract, err := bindFrostDkgValidator(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &FrostDkgValidatorFilterer{contract: contract}, nil +} + +// bindFrostDkgValidator binds a generic wrapper to an already deployed contract. +func bindFrostDkgValidator(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := FrostDkgValidatorMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_FrostDkgValidator *FrostDkgValidatorRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _FrostDkgValidator.Contract.FrostDkgValidatorCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_FrostDkgValidator *FrostDkgValidatorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FrostDkgValidator.Contract.FrostDkgValidatorTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_FrostDkgValidator *FrostDkgValidatorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _FrostDkgValidator.Contract.FrostDkgValidatorTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_FrostDkgValidator *FrostDkgValidatorCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _FrostDkgValidator.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_FrostDkgValidator *FrostDkgValidatorTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _FrostDkgValidator.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_FrostDkgValidator *FrostDkgValidatorTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _FrostDkgValidator.Contract.contract.Transact(opts, method, params...) +} + +// ActiveThreshold is a free data retrieval call binding the contract method 0x281efe71. +// +// Solidity: function activeThreshold() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCaller) ActiveThreshold(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "activeThreshold") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// ActiveThreshold is a free data retrieval call binding the contract method 0x281efe71. +// +// Solidity: function activeThreshold() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorSession) ActiveThreshold() (*big.Int, error) { + return _FrostDkgValidator.Contract.ActiveThreshold(&_FrostDkgValidator.CallOpts) +} + +// ActiveThreshold is a free data retrieval call binding the contract method 0x281efe71. +// +// Solidity: function activeThreshold() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) ActiveThreshold() (*big.Int, error) { + return _FrostDkgValidator.Contract.ActiveThreshold(&_FrostDkgValidator.CallOpts) +} + +// GroupSize is a free data retrieval call binding the contract method 0x63b635ea. +// +// Solidity: function groupSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCaller) GroupSize(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "groupSize") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GroupSize is a free data retrieval call binding the contract method 0x63b635ea. +// +// Solidity: function groupSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorSession) GroupSize() (*big.Int, error) { + return _FrostDkgValidator.Contract.GroupSize(&_FrostDkgValidator.CallOpts) +} + +// GroupSize is a free data retrieval call binding the contract method 0x63b635ea. +// +// Solidity: function groupSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) GroupSize() (*big.Int, error) { + return _FrostDkgValidator.Contract.GroupSize(&_FrostDkgValidator.CallOpts) +} + +// GroupThreshold is a free data retrieval call binding the contract method 0x6dcc64f8. +// +// Solidity: function groupThreshold() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCaller) GroupThreshold(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "groupThreshold") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GroupThreshold is a free data retrieval call binding the contract method 0x6dcc64f8. +// +// Solidity: function groupThreshold() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorSession) GroupThreshold() (*big.Int, error) { + return _FrostDkgValidator.Contract.GroupThreshold(&_FrostDkgValidator.CallOpts) +} + +// GroupThreshold is a free data retrieval call binding the contract method 0x6dcc64f8. +// +// Solidity: function groupThreshold() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) GroupThreshold() (*big.Int, error) { + return _FrostDkgValidator.Contract.GroupThreshold(&_FrostDkgValidator.CallOpts) +} + +// PublicKeyByteSize is a free data retrieval call binding the contract method 0x05f8ae15. +// +// Solidity: function publicKeyByteSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCaller) PublicKeyByteSize(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "publicKeyByteSize") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// PublicKeyByteSize is a free data retrieval call binding the contract method 0x05f8ae15. +// +// Solidity: function publicKeyByteSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorSession) PublicKeyByteSize() (*big.Int, error) { + return _FrostDkgValidator.Contract.PublicKeyByteSize(&_FrostDkgValidator.CallOpts) +} + +// PublicKeyByteSize is a free data retrieval call binding the contract method 0x05f8ae15. +// +// Solidity: function publicKeyByteSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) PublicKeyByteSize() (*big.Int, error) { + return _FrostDkgValidator.Contract.PublicKeyByteSize(&_FrostDkgValidator.CallOpts) +} + +// ResultDigest is a free data retrieval call binding the contract method 0xa63415cd. +// +// Solidity: function resultDigest((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, address bridge, address registry) view returns(bytes32 digest) +func (_FrostDkgValidator *FrostDkgValidatorCaller) ResultDigest(opts *bind.CallOpts, result FrostDkgResult, seed *big.Int, bridge common.Address, registry common.Address) ([32]byte, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "resultDigest", result, seed, bridge, registry) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// ResultDigest is a free data retrieval call binding the contract method 0xa63415cd. +// +// Solidity: function resultDigest((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, address bridge, address registry) view returns(bytes32 digest) +func (_FrostDkgValidator *FrostDkgValidatorSession) ResultDigest(result FrostDkgResult, seed *big.Int, bridge common.Address, registry common.Address) ([32]byte, error) { + return _FrostDkgValidator.Contract.ResultDigest(&_FrostDkgValidator.CallOpts, result, seed, bridge, registry) +} + +// ResultDigest is a free data retrieval call binding the contract method 0xa63415cd. +// +// Solidity: function resultDigest((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, address bridge, address registry) view returns(bytes32 digest) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) ResultDigest(result FrostDkgResult, seed *big.Int, bridge common.Address, registry common.Address) ([32]byte, error) { + return _FrostDkgValidator.Contract.ResultDigest(&_FrostDkgValidator.CallOpts, result, seed, bridge, registry) +} + +// SignatureByteSize is a free data retrieval call binding the contract method 0x89ef44b0. +// +// Solidity: function signatureByteSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCaller) SignatureByteSize(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "signatureByteSize") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// SignatureByteSize is a free data retrieval call binding the contract method 0x89ef44b0. +// +// Solidity: function signatureByteSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorSession) SignatureByteSize() (*big.Int, error) { + return _FrostDkgValidator.Contract.SignatureByteSize(&_FrostDkgValidator.CallOpts) +} + +// SignatureByteSize is a free data retrieval call binding the contract method 0x89ef44b0. +// +// Solidity: function signatureByteSize() view returns(uint256) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) SignatureByteSize() (*big.Int, error) { + return _FrostDkgValidator.Contract.SignatureByteSize(&_FrostDkgValidator.CallOpts) +} + +// SortitionPool is a free data retrieval call binding the contract method 0xb54a2374. +// +// Solidity: function sortitionPool() view returns(address) +func (_FrostDkgValidator *FrostDkgValidatorCaller) SortitionPool(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "sortitionPool") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// SortitionPool is a free data retrieval call binding the contract method 0xb54a2374. +// +// Solidity: function sortitionPool() view returns(address) +func (_FrostDkgValidator *FrostDkgValidatorSession) SortitionPool() (common.Address, error) { + return _FrostDkgValidator.Contract.SortitionPool(&_FrostDkgValidator.CallOpts) +} + +// SortitionPool is a free data retrieval call binding the contract method 0xb54a2374. +// +// Solidity: function sortitionPool() view returns(address) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) SortitionPool() (common.Address, error) { + return _FrostDkgValidator.Contract.SortitionPool(&_FrostDkgValidator.CallOpts) +} + +// Validate is a free data retrieval call binding the contract method 0x8a399fcf. +// +// Solidity: function validate((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, uint256 startBlock, address bridge, address registry) view returns(bool isValid, string errorMsg) +func (_FrostDkgValidator *FrostDkgValidatorCaller) Validate(opts *bind.CallOpts, result FrostDkgResult, seed *big.Int, startBlock *big.Int, bridge common.Address, registry common.Address) (struct { + IsValid bool + ErrorMsg string +}, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "validate", result, seed, startBlock, bridge, registry) + + outstruct := new(struct { + IsValid bool + ErrorMsg string + }) + if err != nil { + return *outstruct, err + } + + outstruct.IsValid = *abi.ConvertType(out[0], new(bool)).(*bool) + outstruct.ErrorMsg = *abi.ConvertType(out[1], new(string)).(*string) + + return *outstruct, err + +} + +// Validate is a free data retrieval call binding the contract method 0x8a399fcf. +// +// Solidity: function validate((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, uint256 startBlock, address bridge, address registry) view returns(bool isValid, string errorMsg) +func (_FrostDkgValidator *FrostDkgValidatorSession) Validate(result FrostDkgResult, seed *big.Int, startBlock *big.Int, bridge common.Address, registry common.Address) (struct { + IsValid bool + ErrorMsg string +}, error) { + return _FrostDkgValidator.Contract.Validate(&_FrostDkgValidator.CallOpts, result, seed, startBlock, bridge, registry) +} + +// Validate is a free data retrieval call binding the contract method 0x8a399fcf. +// +// Solidity: function validate((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, uint256 startBlock, address bridge, address registry) view returns(bool isValid, string errorMsg) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) Validate(result FrostDkgResult, seed *big.Int, startBlock *big.Int, bridge common.Address, registry common.Address) (struct { + IsValid bool + ErrorMsg string +}, error) { + return _FrostDkgValidator.Contract.Validate(&_FrostDkgValidator.CallOpts, result, seed, startBlock, bridge, registry) +} + +// ValidateFields is a free data retrieval call binding the contract method 0x0a51bd1f. +// +// Solidity: function validateFields((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) pure returns(bool isValid, string errorMsg) +func (_FrostDkgValidator *FrostDkgValidatorCaller) ValidateFields(opts *bind.CallOpts, result FrostDkgResult) (struct { + IsValid bool + ErrorMsg string +}, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "validateFields", result) + + outstruct := new(struct { + IsValid bool + ErrorMsg string + }) + if err != nil { + return *outstruct, err + } + + outstruct.IsValid = *abi.ConvertType(out[0], new(bool)).(*bool) + outstruct.ErrorMsg = *abi.ConvertType(out[1], new(string)).(*string) + + return *outstruct, err + +} + +// ValidateFields is a free data retrieval call binding the contract method 0x0a51bd1f. +// +// Solidity: function validateFields((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) pure returns(bool isValid, string errorMsg) +func (_FrostDkgValidator *FrostDkgValidatorSession) ValidateFields(result FrostDkgResult) (struct { + IsValid bool + ErrorMsg string +}, error) { + return _FrostDkgValidator.Contract.ValidateFields(&_FrostDkgValidator.CallOpts, result) +} + +// ValidateFields is a free data retrieval call binding the contract method 0x0a51bd1f. +// +// Solidity: function validateFields((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) pure returns(bool isValid, string errorMsg) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) ValidateFields(result FrostDkgResult) (struct { + IsValid bool + ErrorMsg string +}, error) { + return _FrostDkgValidator.Contract.ValidateFields(&_FrostDkgValidator.CallOpts, result) +} + +// ValidateGroupMembers is a free data retrieval call binding the contract method 0x11ee7310. +// +// Solidity: function validateGroupMembers((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed) view returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorCaller) ValidateGroupMembers(opts *bind.CallOpts, result FrostDkgResult, seed *big.Int) (bool, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "validateGroupMembers", result, seed) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// ValidateGroupMembers is a free data retrieval call binding the contract method 0x11ee7310. +// +// Solidity: function validateGroupMembers((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed) view returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorSession) ValidateGroupMembers(result FrostDkgResult, seed *big.Int) (bool, error) { + return _FrostDkgValidator.Contract.ValidateGroupMembers(&_FrostDkgValidator.CallOpts, result, seed) +} + +// ValidateGroupMembers is a free data retrieval call binding the contract method 0x11ee7310. +// +// Solidity: function validateGroupMembers((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed) view returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) ValidateGroupMembers(result FrostDkgResult, seed *big.Int) (bool, error) { + return _FrostDkgValidator.Contract.ValidateGroupMembers(&_FrostDkgValidator.CallOpts, result, seed) +} + +// ValidateMembersHash is a free data retrieval call binding the contract method 0xd01d1f3f. +// +// Solidity: function validateMembersHash((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) pure returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorCaller) ValidateMembersHash(opts *bind.CallOpts, result FrostDkgResult) (bool, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "validateMembersHash", result) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// ValidateMembersHash is a free data retrieval call binding the contract method 0xd01d1f3f. +// +// Solidity: function validateMembersHash((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) pure returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorSession) ValidateMembersHash(result FrostDkgResult) (bool, error) { + return _FrostDkgValidator.Contract.ValidateMembersHash(&_FrostDkgValidator.CallOpts, result) +} + +// ValidateMembersHash is a free data retrieval call binding the contract method 0xd01d1f3f. +// +// Solidity: function validateMembersHash((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result) pure returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) ValidateMembersHash(result FrostDkgResult) (bool, error) { + return _FrostDkgValidator.Contract.ValidateMembersHash(&_FrostDkgValidator.CallOpts, result) +} + +// ValidateSignatures is a free data retrieval call binding the contract method 0xb03a9444. +// +// Solidity: function validateSignatures((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, uint256 , (address,address) binding) view returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorCaller) ValidateSignatures(opts *bind.CallOpts, result FrostDkgResult, seed *big.Int, arg2 *big.Int, binding FrostDkgValidatorDigestBinding) (bool, error) { + var out []interface{} + err := _FrostDkgValidator.contract.Call(opts, &out, "validateSignatures", result, seed, arg2, binding) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// ValidateSignatures is a free data retrieval call binding the contract method 0xb03a9444. +// +// Solidity: function validateSignatures((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, uint256 , (address,address) binding) view returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorSession) ValidateSignatures(result FrostDkgResult, seed *big.Int, arg2 *big.Int, binding FrostDkgValidatorDigestBinding) (bool, error) { + return _FrostDkgValidator.Contract.ValidateSignatures(&_FrostDkgValidator.CallOpts, result, seed, arg2, binding) +} + +// ValidateSignatures is a free data retrieval call binding the contract method 0xb03a9444. +// +// Solidity: function validateSignatures((uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32) result, uint256 seed, uint256 , (address,address) binding) view returns(bool) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) ValidateSignatures(result FrostDkgResult, seed *big.Int, arg2 *big.Int, binding FrostDkgValidatorDigestBinding) (bool, error) { + return _FrostDkgValidator.Contract.ValidateSignatures(&_FrostDkgValidator.CallOpts, result, seed, arg2, binding) +} diff --git a/pkg/chain/ethereum/frost_bindings_test.go b/pkg/chain/ethereum/frost_bindings_test.go new file mode 100644 index 0000000000..f1f613a60f --- /dev/null +++ b/pkg/chain/ethereum/frost_bindings_test.go @@ -0,0 +1,124 @@ +package ethereum + +import ( + "bytes" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + frostabi "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen/abi" + frostvalidatorabi "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen/validatorabi" +) + +const frostDkgResultTupleSignature = "(uint256,bytes32,uint8[],bytes,uint256[],uint32[],bytes32)" + +func TestFrostGeneratedBindingsUseDeployedDkgResultTupleOrder(t *testing.T) { + expectedFields := []string{ + "SubmitterMemberIndex", + "XOnlyOutputKey", + "MisbehavedMembersIndices", + "Signatures", + "SigningMembersIndices", + "Members", + "MembersHash", + } + + assertStructFieldOrder(t, reflect.TypeOf(frostabi.FrostDkgResult{}), expectedFields) + assertStructFieldOrder( + t, + reflect.TypeOf(frostvalidatorabi.FrostDkgResult{}), + expectedFields, + ) + + walletRegistryABI, err := frostabi.FrostWalletRegistryMetaData.GetAbi() + if err != nil { + t.Fatal(err) + } + + for _, method := range []string{ + "submitDkgResult", + "approveDkgResult", + "challengeDkgResult", + "isDkgResultValid", + } { + expectedSelector := functionSelector( + method + "(" + frostDkgResultTupleSignature + ")", + ) + actualSelector := walletRegistryABI.Methods[method].ID + if !bytes.Equal(actualSelector, expectedSelector) { + t.Fatalf( + "unexpected %s selector: got 0x%x, want 0x%x", + method, + actualSelector, + expectedSelector, + ) + } + } + + expectedEventID := crypto.Keccak256Hash([]byte( + "DkgResultSubmitted(bytes32,uint256," + + frostDkgResultTupleSignature + + ")", + )) + actualEventID := walletRegistryABI.Events["DkgResultSubmitted"].ID + if actualEventID != expectedEventID { + t.Fatalf( + "unexpected DkgResultSubmitted topic: got 0x%x, want 0x%x", + actualEventID, + expectedEventID, + ) + } + + validatorABI, err := frostvalidatorabi.FrostDkgValidatorMetaData.GetAbi() + if err != nil { + t.Fatal(err) + } + + expectedSelector := functionSelector( + "resultDigest(" + + frostDkgResultTupleSignature + + ",uint256,address,address)", + ) + actualSelector := validatorABI.Methods["resultDigest"].ID + if !bytes.Equal(actualSelector, expectedSelector) { + t.Fatalf( + "unexpected resultDigest selector: got 0x%x, want 0x%x", + actualSelector, + expectedSelector, + ) + } +} + +func assertStructFieldOrder( + t *testing.T, + structType reflect.Type, + expectedFields []string, +) { + t.Helper() + + if structType.NumField() != len(expectedFields) { + t.Fatalf( + "unexpected field count for %s: got %d, want %d", + structType.Name(), + structType.NumField(), + len(expectedFields), + ) + } + + for i, expectedField := range expectedFields { + if actualField := structType.Field(i).Name; actualField != expectedField { + t.Fatalf( + "unexpected field %d for %s: got %s, want %s", + i, + structType.Name(), + actualField, + expectedField, + ) + } + } +} + +func functionSelector(signature string) []byte { + hash := crypto.Keccak256([]byte(signature)) + return hash[:4] +} diff --git a/pkg/chain/ethereum/frost_dkg.go b/pkg/chain/ethereum/frost_dkg.go new file mode 100644 index 0000000000..b4f0ab350f --- /dev/null +++ b/pkg/chain/ethereum/frost_dkg.go @@ -0,0 +1,944 @@ +package ethereum + +import ( + "context" + "fmt" + "math/big" + "sort" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + chainutil "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" + "github.com/keep-network/keep-core/pkg/chain" + frostabi "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen/abi" + frostvalidatorabi "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen/validatorabi" + "github.com/keep-network/keep-core/pkg/frost" + frostregistry "github.com/keep-network/keep-core/pkg/frost/registry" + "github.com/keep-network/keep-core/pkg/subscription" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +var _ tbtc.FrostDKGChain = (*TbtcChain)(nil) + +// FrostWalletRegistryAvailable reports whether the chain handle is configured +// with a FROST wallet registry address. +func (tc *TbtcChain) FrostWalletRegistryAvailable() bool { + return tc.frostWalletRegistry != nil +} + +// OnBridgeNewWalletRequested registers a callback for Bridge.NewWalletRequested. +func (tc *TbtcChain) OnBridgeNewWalletRequested( + handler func(event *tbtc.BridgeNewWalletRequestedEvent), +) subscription.EventSubscription { + return tc.bridge.NewWalletRequestedEvent(nil).OnEvent( + func(blockNumber uint64) { + handler(&tbtc.BridgeNewWalletRequestedEvent{ + BlockNumber: blockNumber, + }) + }, + ) +} + +// OnFrostDKGStarted registers a callback for FrostWalletRegistry.DkgStarted. +func (tc *TbtcChain) OnFrostDKGStarted( + handler func(event *tbtc.FrostDKGStartedEvent), +) subscription.EventSubscription { + if tc.frostWalletRegistry == nil { + return subscription.NewEventSubscription(func() {}) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + events := make(chan *tbtc.FrostDKGStartedEvent) + watchSink := make(chan *frostabi.FrostWalletRegistryDkgStarted) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event, ok := <-events: + if !ok { + return + } + handler(event) + } + } + }() + + go func() { + for { + select { + case <-ctx.Done(): + return + case event, ok := <-watchSink: + if !ok { + return + } + emitFrostDKGStartedEvent( + ctx, + events, + &tbtc.FrostDKGStartedEvent{ + Seed: event.Seed, + BlockNumber: event.Raw.BlockNumber, + }, + ) + } + } + }() + + go tc.monitorPastFrostDKGStartedEvents(ctx, events) + + sub := tc.watchFrostDKGStarted(watchSink, nil) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +// PastFrostDKGStartedEvents fetches past FROST DKG started events. +func (tc *TbtcChain) PastFrostDKGStartedEvents( + filter *tbtc.FrostDKGStartedEventFilter, +) ([]*tbtc.FrostDKGStartedEvent, error) { + if tc.frostWalletRegistry == nil { + return nil, fmt.Errorf("FrostWalletRegistry is not configured") + } + + var startBlock uint64 + var endBlock *uint64 + var seed []*big.Int + + if filter != nil { + startBlock = filter.StartBlock + endBlock = filter.EndBlock + seed = filter.Seed + } + + iterator, err := tc.frostWalletRegistry.FilterDkgStarted( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + seed, + ) + if err != nil { + return nil, err + } + defer iterator.Close() + + events := make([]*tbtc.FrostDKGStartedEvent, 0) + for iterator.Next() { + events = append(events, &tbtc.FrostDKGStartedEvent{ + Seed: iterator.Event.Seed, + BlockNumber: iterator.Event.Raw.BlockNumber, + }) + } + if err := iterator.Error(); err != nil { + return nil, err + } + + sort.SliceStable(events, func(i, j int) bool { + return events[i].BlockNumber < events[j].BlockNumber + }) + + return events, nil +} + +// OnFrostDKGResultSubmitted registers a callback for FROST DKG submissions. +func (tc *TbtcChain) OnFrostDKGResultSubmitted( + handler func(event *tbtc.FrostDKGResultSubmittedEvent), +) subscription.EventSubscription { + if tc.frostWalletRegistry == nil { + return subscription.NewEventSubscription(func() {}) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + events := make(chan *tbtc.FrostDKGResultSubmittedEvent) + watchSink := make(chan *frostabi.FrostWalletRegistryDkgResultSubmitted) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event, ok := <-events: + if !ok { + return + } + handler(event) + } + } + }() + + go func() { + for { + select { + case <-ctx.Done(): + return + case event, ok := <-watchSink: + if !ok { + return + } + result, err := convertFrostDKGResultFromABI(event.Result) + if err != nil { + logger.Errorf("unexpected FROST DKG result in event: [%v]", err) + continue + } + + emitFrostDKGResultSubmittedEvent( + ctx, + events, + &tbtc.FrostDKGResultSubmittedEvent{ + Seed: event.Seed, + ResultHash: event.ResultHash, + Result: result, + BlockNumber: event.Raw.BlockNumber, + }, + ) + } + } + }() + + go tc.monitorPastFrostDKGResultSubmittedEvents(ctx, events) + + sub := tc.watchFrostDKGResultSubmitted(watchSink, nil, nil) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +// PastFrostDKGResultSubmittedEvents fetches past FROST DKG submitted events. +func (tc *TbtcChain) PastFrostDKGResultSubmittedEvents( + filter *tbtc.FrostDKGResultSubmittedEventFilter, +) ([]*tbtc.FrostDKGResultSubmittedEvent, error) { + if tc.frostWalletRegistry == nil { + return nil, fmt.Errorf("FrostWalletRegistry is not configured") + } + + var startBlock uint64 + var endBlock *uint64 + var resultHash [][32]byte + var seed []*big.Int + + if filter != nil { + startBlock = filter.StartBlock + endBlock = filter.EndBlock + for _, hash := range filter.ResultHash { + resultHash = append(resultHash, [32]byte(hash)) + } + seed = filter.Seed + } + + iterator, err := tc.frostWalletRegistry.FilterDkgResultSubmitted( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + resultHash, + seed, + ) + if err != nil { + return nil, err + } + defer iterator.Close() + + events := make([]*tbtc.FrostDKGResultSubmittedEvent, 0) + for iterator.Next() { + result, err := convertFrostDKGResultFromABI(iterator.Event.Result) + if err != nil { + return nil, err + } + + events = append(events, &tbtc.FrostDKGResultSubmittedEvent{ + Seed: iterator.Event.Seed, + ResultHash: iterator.Event.ResultHash, + Result: result, + BlockNumber: iterator.Event.Raw.BlockNumber, + }) + } + if err := iterator.Error(); err != nil { + return nil, err + } + + sort.SliceStable(events, func(i, j int) bool { + return events[i].BlockNumber < events[j].BlockNumber + }) + + return events, nil +} + +func (tc *TbtcChain) watchFrostDKGStarted( + sink chan<- *frostabi.FrostWalletRegistryDkgStarted, + seed []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return tc.frostWalletRegistry.WatchDkgStarted( + &bind.WatchOpts{Context: ctx}, + sink, + seed, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + logger.Warnf( + "subscription to FROST DkgStarted had to be retried [%s] "+ + "since the last attempt; please inspect host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + logger.Errorf( + "subscription to FROST DkgStarted failed with error: [%v]; "+ + "resubscription attempt will be performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (tc *TbtcChain) watchFrostDKGResultSubmitted( + sink chan<- *frostabi.FrostWalletRegistryDkgResultSubmitted, + resultHash [][32]byte, + seed []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return tc.frostWalletRegistry.WatchDkgResultSubmitted( + &bind.WatchOpts{Context: ctx}, + sink, + resultHash, + seed, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + logger.Warnf( + "subscription to FROST DkgResultSubmitted had to be retried [%s] "+ + "since the last attempt; please inspect host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + logger.Errorf( + "subscription to FROST DkgResultSubmitted failed with error: [%v]; "+ + "resubscription attempt will be performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (tc *TbtcChain) monitorPastFrostDKGStartedEvents( + ctx context.Context, + events chan<- *tbtc.FrostDKGStartedEvent, +) { + ticker := time.NewTicker(chainutil.DefaultSubscribeOptsTick) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := tc.blockCounter.CurrentBlock() + if err != nil { + logger.Errorf( + "FROST DkgStarted subscription failed to pull events: [%v]", + err, + ) + continue + } + + fromBlock := frostSubscriptionMonitoringStartBlock(lastBlock) + logger.Infof( + "FROST DkgStarted subscription monitoring fetching past "+ + "events starting from block [%v]", + fromBlock, + ) + + pastEvents, err := tc.PastFrostDKGStartedEvents( + &tbtc.FrostDKGStartedEventFilter{StartBlock: fromBlock}, + ) + if err != nil { + logger.Errorf( + "FROST DkgStarted subscription failed to pull events: [%v]", + err, + ) + continue + } + + logger.Infof( + "FROST DkgStarted subscription monitoring fetched [%v] past events", + len(pastEvents), + ) + + for _, event := range pastEvents { + emitFrostDKGStartedEvent(ctx, events, event) + } + } + } +} + +func (tc *TbtcChain) monitorPastFrostDKGResultSubmittedEvents( + ctx context.Context, + events chan<- *tbtc.FrostDKGResultSubmittedEvent, +) { + ticker := time.NewTicker(chainutil.DefaultSubscribeOptsTick) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := tc.blockCounter.CurrentBlock() + if err != nil { + logger.Errorf( + "FROST DkgResultSubmitted subscription failed to pull events: [%v]", + err, + ) + continue + } + + fromBlock := frostSubscriptionMonitoringStartBlock(lastBlock) + logger.Infof( + "FROST DkgResultSubmitted subscription monitoring fetching past "+ + "events starting from block [%v]", + fromBlock, + ) + + pastEvents, err := tc.PastFrostDKGResultSubmittedEvents( + &tbtc.FrostDKGResultSubmittedEventFilter{StartBlock: fromBlock}, + ) + if err != nil { + logger.Errorf( + "FROST DkgResultSubmitted subscription failed to pull events: [%v]", + err, + ) + continue + } + + logger.Infof( + "FROST DkgResultSubmitted subscription monitoring fetched [%v] past events", + len(pastEvents), + ) + + for _, event := range pastEvents { + emitFrostDKGResultSubmittedEvent(ctx, events, event) + } + } + } +} + +func frostSubscriptionMonitoringStartBlock(lastBlock uint64) uint64 { + pastBlocks := uint64(chainutil.DefaultSubscribeOptsPastBlocks) + if lastBlock <= pastBlocks { + return 0 + } + + return lastBlock - pastBlocks +} + +func emitFrostDKGStartedEvent( + ctx context.Context, + events chan<- *tbtc.FrostDKGStartedEvent, + event *tbtc.FrostDKGStartedEvent, +) { + select { + case <-ctx.Done(): + case events <- event: + } +} + +func emitFrostDKGResultSubmittedEvent( + ctx context.Context, + events chan<- *tbtc.FrostDKGResultSubmittedEvent, + event *tbtc.FrostDKGResultSubmittedEvent, +) { + select { + case <-ctx.Done(): + case events <- event: + } +} + +// OnFrostDKGResultChallenged registers a callback for FROST DKG challenges. +func (tc *TbtcChain) OnFrostDKGResultChallenged( + handler func(event *tbtc.FrostDKGResultChallengedEvent), +) subscription.EventSubscription { + if tc.frostWalletRegistry == nil { + return subscription.NewEventSubscription(func() {}) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + sink := make(chan *frostabi.FrostWalletRegistryDkgResultChallenged) + + sub, err := tc.frostWalletRegistry.WatchDkgResultChallenged( + &bind.WatchOpts{Context: ctx}, + sink, + nil, + nil, + ) + if err != nil { + cancelCtx() + logger.Errorf("failed to watch FROST DKG challenged events: [%v]", err) + return subscription.NewEventSubscription(func() {}) + } + + go func() { + for { + select { + case <-ctx.Done(): + return + case err, ok := <-sub.Err(): + if !ok { + return + } + logger.Errorf("FROST DKG challenged subscription error: [%v]", err) + case event, ok := <-sink: + if !ok { + return + } + handler(&tbtc.FrostDKGResultChallengedEvent{ + ResultHash: event.ResultHash, + Challenger: chain.Address(event.Challenger.String()), + Reason: event.Reason, + BlockNumber: event.Raw.BlockNumber, + }) + } + } + }() + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +// OnFrostDKGResultApproved registers a callback for FROST DKG approvals. +func (tc *TbtcChain) OnFrostDKGResultApproved( + handler func(event *tbtc.FrostDKGResultApprovedEvent), +) subscription.EventSubscription { + if tc.frostWalletRegistry == nil { + return subscription.NewEventSubscription(func() {}) + } + + ctx, cancelCtx := context.WithCancel(context.Background()) + sink := make(chan *frostabi.FrostWalletRegistryDkgResultApproved) + + sub, err := tc.frostWalletRegistry.WatchDkgResultApproved( + &bind.WatchOpts{Context: ctx}, + sink, + nil, + nil, + ) + if err != nil { + cancelCtx() + logger.Errorf("failed to watch FROST DKG approved events: [%v]", err) + return subscription.NewEventSubscription(func() {}) + } + + go func() { + for { + select { + case <-ctx.Done(): + return + case err, ok := <-sub.Err(): + if !ok { + return + } + logger.Errorf("FROST DKG approved subscription error: [%v]", err) + case event, ok := <-sink: + if !ok { + return + } + handler(&tbtc.FrostDKGResultApprovedEvent{ + ResultHash: event.ResultHash, + Approver: chain.Address(event.Approver.String()), + BlockNumber: event.Raw.BlockNumber, + }) + } + } + }() + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +// SelectFrostGroup returns the currently selected FROST DKG group. +func (tc *TbtcChain) SelectFrostGroup() (*tbtc.GroupSelectionResult, error) { + if tc.frostWalletRegistry == nil || tc.frostSortitionPool == nil { + return nil, fmt.Errorf("FrostWalletRegistry is not configured") + } + + operatorsIDs, err := tc.frostWalletRegistry.SelectGroup( + &bind.CallOpts{From: tc.key.Address}, + ) + if err != nil { + return nil, err + } + + operatorsAddresses, err := tc.frostSortitionPool.GetIDOperators(operatorsIDs) + if err != nil { + return nil, err + } + + ids := make([]chain.OperatorID, len(operatorsIDs)) + addresses := make([]chain.Address, len(operatorsIDs)) + for i := range ids { + ids[i] = operatorsIDs[i] + addresses[i] = chain.Address(operatorsAddresses[i].String()) + } + + return &tbtc.GroupSelectionResult{ + OperatorsIDs: ids, + OperatorsAddresses: addresses, + }, nil +} + +// GetFrostDKGState returns the current FROST wallet creation state. +func (tc *TbtcChain) GetFrostDKGState() (tbtc.DKGState, error) { + if tc.frostWalletRegistry == nil { + return 0, fmt.Errorf("FrostWalletRegistry is not configured") + } + + state, err := tc.frostWalletRegistry.GetWalletCreationState( + &bind.CallOpts{From: tc.key.Address}, + ) + if err != nil { + return 0, err + } + + return tbtc.DKGState(state), nil +} + +// IsFrostDKGResultValid validates the submitted FROST DKG result using the +// registry-level view. This intentionally avoids passing seed/startBlock from +// off-chain code. +func (tc *TbtcChain) IsFrostDKGResultValid( + result *frostregistry.Result, +) (bool, string, error) { + if tc.frostWalletRegistry == nil { + return false, "", fmt.Errorf("FrostWalletRegistry is not configured") + } + + abiResult, err := convertFrostDKGResultToABI(result) + if err != nil { + return false, "", err + } + + return tc.frostWalletRegistry.IsDkgResultValid( + &bind.CallOpts{From: tc.key.Address}, + abiResult, + ) +} + +// CalculateFrostDKGResultDigest computes the pre-EIP-191 result digest and, +// when the validator view is configured, checks it against the on-chain +// FrostDkgValidator.resultDigest implementation. +func (tc *TbtcChain) CalculateFrostDKGResultDigest( + seed *big.Int, + result *frostregistry.Result, +) ([32]byte, error) { + if result == nil { + return [32]byte{}, fmt.Errorf("FROST DKG result is nil") + } + if tc.frostWalletRegistry == nil { + return [32]byte{}, fmt.Errorf("FrostWalletRegistry is not configured") + } + + localDigest, err := frostregistry.ResultDigest( + tc.chainID, + tc.bridgeAddress, + tc.frostWalletRegistryAddr, + seed, + result.XOnlyOutputKey, + result.Members, + result.MisbehavedMembersIndices, + ) + if err != nil { + return [32]byte{}, err + } + + if tc.frostDkgValidator == nil { + return localDigest, nil + } + + validatorResult, err := convertFrostDKGResultToValidatorABI(result) + if err != nil { + return [32]byte{}, err + } + + onChainDigest, err := tc.frostDkgValidator.ResultDigest( + &bind.CallOpts{From: tc.key.Address}, + validatorResult, + seed, + tc.bridgeAddress, + tc.frostWalletRegistryAddr, + ) + if err != nil { + return [32]byte{}, err + } + + if localDigest != onChainDigest { + return [32]byte{}, fmt.Errorf( + "local FROST DKG digest [0x%x] does not match validator digest [0x%x]", + localDigest, + onChainDigest, + ) + } + + return localDigest, nil +} + +// SubmitFrostDKGResult submits a FROST DKG result. Submission is optimistic on +// chain; callers should pre-validate before invoking this method. +func (tc *TbtcChain) SubmitFrostDKGResult(result *frostregistry.Result) error { + if tc.frostWalletRegistry == nil { + return fmt.Errorf("FrostWalletRegistry is not configured") + } + + abiResult, err := convertFrostDKGResultToABI(result) + if err != nil { + return err + } + + return tc.submitFrostWalletRegistryTransaction( + "submitDkgResult", + func(opts *bind.TransactOpts) (*types.Transaction, error) { + return tc.frostWalletRegistry.SubmitDkgResult(opts, abiResult) + }, + ) +} + +// ChallengeFrostDKGResult challenges an invalid FROST DKG result. The on-chain +// function requires msg.sender == tx.origin, which this EOA chain handle +// satisfies directly. +func (tc *TbtcChain) ChallengeFrostDKGResult(result *frostregistry.Result) error { + if tc.frostWalletRegistry == nil { + return fmt.Errorf("FrostWalletRegistry is not configured") + } + + abiResult, err := convertFrostDKGResultToABI(result) + if err != nil { + return err + } + + return tc.submitFrostWalletRegistryTransaction( + "challengeDkgResult", + func(opts *bind.TransactOpts) (*types.Transaction, error) { + return tc.frostWalletRegistry.ChallengeDkgResult(opts, abiResult) + }, + ) +} + +// ApproveFrostDKGResult approves a FROST DKG result after the challenge window. +// The contract gates submitter precedence using submitterPrecedencePeriodLength. +func (tc *TbtcChain) ApproveFrostDKGResult(result *frostregistry.Result) error { + if tc.frostWalletRegistry == nil { + return fmt.Errorf("FrostWalletRegistry is not configured") + } + + abiResult, err := convertFrostDKGResultToABI(result) + if err != nil { + return err + } + + return tc.submitFrostWalletRegistryTransaction( + "approveDkgResult", + func(opts *bind.TransactOpts) (*types.Transaction, error) { + return tc.frostWalletRegistry.ApproveDkgResult(opts, abiResult) + }, + ) +} + +// FrostDKGParameters gets the current FROST DKG timing parameters. +func (tc *TbtcChain) FrostDKGParameters() (*tbtc.DKGParameters, error) { + if tc.frostWalletRegistry == nil { + return nil, fmt.Errorf("FrostWalletRegistry is not configured") + } + + params, err := tc.frostWalletRegistry.DkgParameters( + &bind.CallOpts{From: tc.key.Address}, + ) + if err != nil { + return nil, err + } + + return &tbtc.DKGParameters{ + SubmissionTimeoutBlocks: params.ResultSubmissionTimeout.Uint64(), + ChallengePeriodBlocks: params.ResultChallengePeriodLength.Uint64(), + ApprovePrecedencePeriodBlocks: params.SubmitterPrecedencePeriodLength.Uint64(), + }, nil +} + +func convertFrostDKGResultFromABI( + result frostabi.FrostDkgResult, +) (*frostregistry.Result, error) { + submitterMemberIndex, err := uint256ToUint64( + result.SubmitterMemberIndex, + "submitter member index", + ) + if err != nil { + return nil, err + } + + signingMembersIndices := make([]uint64, len(result.SigningMembersIndices)) + for i, signingMemberIndex := range result.SigningMembersIndices { + signingMembersIndices[i], err = uint256ToUint64( + signingMemberIndex, + "signing member index", + ) + if err != nil { + return nil, err + } + } + + outputKey := frost.OutputKey(result.XOnlyOutputKey) + + return &frostregistry.Result{ + SubmitterMemberIndex: submitterMemberIndex, + XOnlyOutputKey: outputKey, + MembersHash: result.MembersHash, + MisbehavedMembersIndices: append(frostregistry.MisbehavedMemberIndices{}, result.MisbehavedMembersIndices...), + Signatures: append([]byte{}, result.Signatures...), + SigningMembersIndices: signingMembersIndices, + Members: append(frostregistry.FullMembers{}, result.Members...), + }, nil +} + +func convertFrostDKGResultToABI( + result *frostregistry.Result, +) (frostabi.FrostDkgResult, error) { + if result == nil { + return frostabi.FrostDkgResult{}, fmt.Errorf("FROST DKG result is nil") + } + + signingMembersIndices := make([]*big.Int, len(result.SigningMembersIndices)) + for i, signingMemberIndex := range result.SigningMembersIndices { + signingMembersIndices[i] = new(big.Int).SetUint64(signingMemberIndex) + } + + return frostabi.FrostDkgResult{ + SubmitterMemberIndex: new(big.Int).SetUint64(result.SubmitterMemberIndex), + XOnlyOutputKey: [32]byte(result.XOnlyOutputKey), + MisbehavedMembersIndices: append([]uint8{}, result.MisbehavedMembersIndices...), + Signatures: append([]byte{}, result.Signatures...), + SigningMembersIndices: signingMembersIndices, + Members: append([]uint32{}, result.Members...), + MembersHash: result.MembersHash, + }, nil +} + +func convertFrostDKGResultToValidatorABI( + result *frostregistry.Result, +) (frostvalidatorabi.FrostDkgResult, error) { + if result == nil { + return frostvalidatorabi.FrostDkgResult{}, fmt.Errorf("FROST DKG result is nil") + } + + signingMembersIndices := make([]*big.Int, len(result.SigningMembersIndices)) + for i, signingMemberIndex := range result.SigningMembersIndices { + signingMembersIndices[i] = new(big.Int).SetUint64(signingMemberIndex) + } + + return frostvalidatorabi.FrostDkgResult{ + SubmitterMemberIndex: new(big.Int).SetUint64(result.SubmitterMemberIndex), + XOnlyOutputKey: [32]byte(result.XOnlyOutputKey), + MisbehavedMembersIndices: append([]uint8{}, result.MisbehavedMembersIndices...), + Signatures: append([]byte{}, result.Signatures...), + SigningMembersIndices: signingMembersIndices, + Members: append([]uint32{}, result.Members...), + MembersHash: result.MembersHash, + }, nil +} + +func (tc *TbtcChain) submitFrostWalletRegistryTransaction( + method string, + submitFn func(opts *bind.TransactOpts) (*types.Transaction, error), +) error { + tc.transactionMutex.Lock() + defer tc.transactionMutex.Unlock() + + transactorOptions, err := bind.NewKeyedTransactorWithChainID( + tc.key.PrivateKey, + tc.chainID, + ) + if err != nil { + return fmt.Errorf("failed to instantiate transactor: [%v]", err) + } + + nonce, err := tc.nonceManager.CurrentNonce() + if err != nil { + return fmt.Errorf("failed to retrieve account nonce: [%v]", err) + } + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := submitFn(transactorOptions) + if err != nil { + return fmt.Errorf("failed to submit %s transaction: [%w]", method, err) + } + + logger.Infof( + "submitted transaction %s with id: [%s] and nonce [%v]", + method, + transaction.Hash(), + transaction.Nonce(), + ) + + go tc.miningWaiter.ForceMining( + transaction, + transactorOptions, + func(newTransactorOptions *bind.TransactOpts) (*types.Transaction, error) { + transaction, err := submitFn(newTransactorOptions) + if err != nil { + return nil, err + } + + logger.Infof( + "submitted transaction %s with id: [%s] and nonce [%v]", + method, + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, + ) + + tc.nonceManager.IncrementNonce() + + return nil +} + +func uint256ToUint64(value *big.Int, fieldName string) (uint64, error) { + if value == nil { + return 0, fmt.Errorf("%s is nil", fieldName) + } + + if !value.IsUint64() { + return 0, fmt.Errorf("%s [%s] overflows uint64", fieldName, value.String()) + } + + return value.Uint64(), nil +} diff --git a/pkg/chain/ethereum/frost_dkg_test.go b/pkg/chain/ethereum/frost_dkg_test.go new file mode 100644 index 0000000000..51fef57385 --- /dev/null +++ b/pkg/chain/ethereum/frost_dkg_test.go @@ -0,0 +1,21 @@ +package ethereum + +import ( + "testing" + + frostabi "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen/abi" +) + +func TestTbtcChainFrostWalletRegistryAvailable(t *testing.T) { + chainWithoutFrostRegistry := &TbtcChain{} + if chainWithoutFrostRegistry.FrostWalletRegistryAvailable() { + t.Fatal("expected FROST wallet registry to be unavailable") + } + + chainWithFrostRegistry := &TbtcChain{ + frostWalletRegistry: &frostabi.FrostWalletRegistry{}, + } + if !chainWithFrostRegistry.FrostWalletRegistryAvailable() { + t.Fatal("expected FROST wallet registry to be available") + } +} diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index ec5c29d40f..20658b15c1 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -8,12 +8,15 @@ import ( "math/big" "reflect" "sort" + "strings" "time" "github.com/keep-network/keep-common/pkg/cache" "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" "github.com/keep-network/keep-core/pkg/bitcoin" @@ -22,6 +25,8 @@ import ( "github.com/keep-network/keep-core/pkg/chain" ecdsaabi "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen/abi" ecdsacontract "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen/contract" + frostabi "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen/abi" + frostvalidatorabi "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen/validatorabi" tbtcabi "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi" tbtccontract "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/contract" "github.com/keep-network/keep-core/pkg/internal/byteutils" @@ -38,6 +43,8 @@ const ( // TODO: The WalletRegistry address is taken from the Bridge contract. // Remove the possibility of passing it through the config. WalletRegistryContractName = "WalletRegistry" + FrostWalletRegistryContractName = "FrostWalletRegistry" + FrostDkgValidatorContractName = "FrostDkgValidator" BridgeContractName = "Bridge" MaintainerProxyContractName = "MaintainerProxy" WalletProposalValidatorContractName = "WalletProposalValidator" @@ -47,14 +54,40 @@ const ( sweptDepositsCachePeriod = 7 * 24 * time.Hour ) +const frostWalletRegistryAuthorizationViewsABI = `[ + { + "inputs": [{"internalType": "address", "name": "operator", "type": "address"}], + "name": "operatorToStakingProvider", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "stakingProvider", "type": "address"}], + "name": "eligibleStake", + "outputs": [{"internalType": "uint96", "name": "", "type": "uint96"}], + "stateMutability": "view", + "type": "function" + } +]` + +var frostWalletRegistryAuthorizationABI = mustParseABI( + frostWalletRegistryAuthorizationViewsABI, +) + // TbtcChain represents a TBTC-specific chain handle. type TbtcChain struct { *baseChain bridge *tbtccontract.Bridge + bridgeAddress common.Address maintainerProxy *tbtccontract.MaintainerProxy walletRegistry *ecdsacontract.WalletRegistry sortitionPool *ecdsacontract.EcdsaSortitionPool + frostWalletRegistry *frostabi.FrostWalletRegistry + frostWalletRegistryAddr common.Address + frostDkgValidator *frostvalidatorabi.FrostDkgValidator + frostSortitionPool *ecdsacontract.EcdsaSortitionPool walletProposalValidator *tbtccontract.WalletProposalValidator redemptionWatchtower *tbtccontract.RedemptionWatchtower @@ -175,6 +208,19 @@ func newTbtcChain( ) } + frostWalletRegistry, frostWalletRegistryAddr, frostSortitionPool, err := connectFrostWalletRegistry( + config, + baseChain, + ) + if err != nil { + return nil, err + } + + frostDkgValidator, err := connectFrostDkgValidator(config, baseChain) + if err != nil { + return nil, err + } + walletProposalValidatorAddress, err := config.ContractAddress( WalletProposalValidatorContractName, ) @@ -241,15 +287,133 @@ func newTbtcChain( return &TbtcChain{ baseChain: baseChain, bridge: bridge, + bridgeAddress: bridgeAddress, maintainerProxy: maintainerProxy, walletRegistry: walletRegistry, sortitionPool: sortitionPool, + frostWalletRegistry: frostWalletRegistry, + frostWalletRegistryAddr: frostWalletRegistryAddr, + frostDkgValidator: frostDkgValidator, + frostSortitionPool: frostSortitionPool, walletProposalValidator: walletProposalValidator, redemptionWatchtower: redemptionWatchtower, sweptDepositsCache: cache.NewGenericTimeCache[*tbtc.DepositChainRequest](sweptDepositsCachePeriod), }, nil } +func connectFrostWalletRegistry( + config ethereum.Config, + baseChain *baseChain, +) ( + *frostabi.FrostWalletRegistry, + common.Address, + *ecdsacontract.EcdsaSortitionPool, + error, +) { + frostWalletRegistryAddress, err := config.ContractAddress( + FrostWalletRegistryContractName, + ) + if err != nil { + return nil, common.Address{}, nil, fmt.Errorf( + "failed to resolve %s contract address: [%v]", + FrostWalletRegistryContractName, + err, + ) + } + + if frostWalletRegistryAddress == (common.Address{}) { + logger.Infof( + "%s contract address not configured; FROST DKG coordinator disabled", + FrostWalletRegistryContractName, + ) + return nil, common.Address{}, nil, nil + } + + frostWalletRegistry, err := frostabi.NewFrostWalletRegistry( + frostWalletRegistryAddress, + baseChain.client, + ) + if err != nil { + return nil, common.Address{}, nil, fmt.Errorf( + "failed to attach to FrostWalletRegistry contract: [%v]", + err, + ) + } + + frostSortitionPoolAddress, err := frostWalletRegistry.SortitionPool( + &bind.CallOpts{From: baseChain.key.Address}, + ) + if err != nil { + return nil, common.Address{}, nil, fmt.Errorf( + "failed to get FROST sortition pool address: [%v]", + err, + ) + } + + // The FROST deployment uses a dedicated sortition pool instance but the + // SortitionPool ABI is the same shape as the ECDSA pool binding. + frostSortitionPool, err := ecdsacontract.NewEcdsaSortitionPool( + frostSortitionPoolAddress, + baseChain.chainID, + baseChain.key, + baseChain.client, + baseChain.nonceManager, + baseChain.miningWaiter, + baseChain.blockCounter, + baseChain.transactionMutex, + ) + if err != nil { + return nil, common.Address{}, nil, fmt.Errorf( + "failed to attach to FrostSortitionPool contract: [%v]", + err, + ) + } + + return frostWalletRegistry, frostWalletRegistryAddress, frostSortitionPool, nil +} + +func (tc *TbtcChain) hasFrostAuthorization() bool { + return tc.frostWalletRegistry != nil && tc.frostSortitionPool != nil +} + +func connectFrostDkgValidator( + config ethereum.Config, + baseChain *baseChain, +) (*frostvalidatorabi.FrostDkgValidator, error) { + frostDkgValidatorAddress, err := config.ContractAddress( + FrostDkgValidatorContractName, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to resolve %s contract address: [%v]", + FrostDkgValidatorContractName, + err, + ) + } + + if frostDkgValidatorAddress == (common.Address{}) { + logger.Infof( + "%s contract address not configured; pre-submit FROST digest "+ + "view checks disabled", + FrostDkgValidatorContractName, + ) + return nil, nil + } + + frostDkgValidator, err := frostvalidatorabi.NewFrostDkgValidator( + frostDkgValidatorAddress, + baseChain.client, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to attach to FrostDkgValidator contract: [%v]", + err, + ) + } + + return frostDkgValidator, nil +} + // Staking returns address of the TokenStaking contract the WalletRegistry is // connected to. func (tc *TbtcChain) Staking() (chain.Address, error) { @@ -265,8 +429,9 @@ func (tc *TbtcChain) Staking() (chain.Address, error) { } // IsRecognized checks whether the given operator is recognized by the TbtcChain -// as eligible to join the network. If the operator has a stake delegation or -// had a stake delegation in the past, it will be recognized. +// as eligible to join the network. Legacy ECDSA operators are recognized if +// they have or had a stake delegation. FROST operators are recognized if the +// FROST registry maps them to a provider with non-zero eligible weight. func (tc *TbtcChain) IsRecognized(operatorPublicKey *operator.PublicKey) (bool, error) { operatorAddress, err := operatorPublicKeyToChainAddress(operatorPublicKey) if err != nil { @@ -287,28 +452,123 @@ func (tc *TbtcChain) IsRecognized(operatorPublicKey *operator.PublicKey) (bool, ) } + if (stakingProvider != common.Address{}) { + // Check if the staking provider has an owner. This check ensures that there + // is/was a stake delegation for the given staking provider. + _, _, _, hasStakeDelegation, err := tc.baseChain.RolesOf( + chain.Address(stakingProvider.Hex()), + ) + if err != nil { + return false, fmt.Errorf( + "failed to check stake delegation for staking provider [%v]: [%v]", + stakingProvider, + err, + ) + } + + if hasStakeDelegation { + return true, nil + } + } + + isRecognizedByFrost, err := tc.isRecognizedByFrostRegistry(operatorAddress) + if err != nil { + return false, err + } + if !isRecognizedByFrost { + return false, nil + } + + return true, nil +} + +func (tc *TbtcChain) isRecognizedByFrostRegistry( + operatorAddress common.Address, +) (bool, error) { + if !tc.hasFrostAuthorization() || + (tc.frostWalletRegistryAddr == common.Address{}) { + return false, nil + } + + out, err := tc.callFrostRegistryAuthorizationView( + "operatorToStakingProvider", + operatorAddress, + ) + if err != nil { + return false, fmt.Errorf( + "failed to map FROST operator [%v] to a provider: [%v]", + operatorAddress, + err, + ) + } + if len(out) != 1 { + return false, fmt.Errorf( + "unexpected FROST operatorToStakingProvider result length [%v]", + len(out), + ) + } + + stakingProvider := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) if (stakingProvider == common.Address{}) { return false, nil } - // Check if the staking provider has an owner. This check ensures that there - // is/was a stake delegation for the given staking provider. - _, _, _, hasStakeDelegation, err := tc.baseChain.RolesOf( - chain.Address(stakingProvider.Hex()), + out, err = tc.callFrostRegistryAuthorizationView( + "eligibleStake", + stakingProvider, ) if err != nil { return false, fmt.Errorf( - "failed to check stake delegation for staking provider [%v]: [%v]", + "failed to get FROST eligible weight for provider [%v]: [%v]", stakingProvider, err, ) } + if len(out) != 1 { + return false, fmt.Errorf( + "unexpected FROST eligibleStake result length [%v]", + len(out), + ) + } - if !hasStakeDelegation { - return false, nil + eligibleWeight := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + return eligibleWeight.Sign() > 0, nil +} + +func (tc *TbtcChain) callFrostRegistryAuthorizationView( + method string, + args ...interface{}, +) ([]interface{}, error) { + var out []interface{} + + contract := bind.NewBoundContract( + tc.frostWalletRegistryAddr, + frostWalletRegistryAuthorizationABI, + tc.baseChain.client, + nil, + nil, + ) + + err := contract.Call( + &bind.CallOpts{From: tc.key.Address}, + &out, + method, + args..., + ) + if err != nil { + return nil, err } - return true, nil + return out, nil +} + +func mustParseABI(rawABI string) abi.ABI { + parsed, err := abi.JSON(strings.NewReader(rawABI)) + if err != nil { + panic(err) + } + + return parsed } // OperatorToStakingProvider returns the staking provider address for the @@ -317,7 +577,18 @@ func (tc *TbtcChain) IsRecognized(operatorPublicKey *operator.PublicKey) (bool, // false. If the staking provider has been registered, the address is not // empty and the boolean flag indicates true. func (tc *TbtcChain) OperatorToStakingProvider() (chain.Address, bool, error) { - stakingProvider, err := tc.walletRegistry.OperatorToStakingProvider(tc.key.Address) + var stakingProvider common.Address + var err error + + if tc.hasFrostAuthorization() { + stakingProvider, err = tc.frostWalletRegistry.OperatorToStakingProvider( + &bind.CallOpts{From: tc.key.Address}, + tc.key.Address, + ) + } else { + stakingProvider, err = tc.walletRegistry.OperatorToStakingProvider(tc.key.Address) + } + if err != nil { return "", false, fmt.Errorf( "failed to map operator [%v] to a staking provider: [%v]", @@ -340,9 +611,20 @@ func (tc *TbtcChain) OperatorToStakingProvider() (chain.Address, bool, error) { // If the authorized stake minus the pending authorization decrease // is below the minimum authorization, eligible stake is 0. func (tc *TbtcChain) EligibleStake(stakingProvider chain.Address) (*big.Int, error) { - eligibleStake, err := tc.walletRegistry.EligibleStake( - common.HexToAddress(stakingProvider.String()), - ) + stakingProviderAddress := common.HexToAddress(stakingProvider.String()) + + var eligibleStake *big.Int + var err error + + if tc.hasFrostAuthorization() { + eligibleStake, err = tc.frostWalletRegistry.EligibleStake( + &bind.CallOpts{From: tc.key.Address}, + stakingProviderAddress, + ) + } else { + eligibleStake, err = tc.walletRegistry.EligibleStake(stakingProviderAddress) + } + if err != nil { return nil, fmt.Errorf( "failed to get eligible stake for staking provider %s: [%w]", @@ -357,12 +639,23 @@ func (tc *TbtcChain) EligibleStake(stakingProvider chain.Address) (*big.Int, err // IsPoolLocked returns true if the sortition pool is locked and no state // changes are allowed. func (tc *TbtcChain) IsPoolLocked() (bool, error) { + if tc.hasFrostAuthorization() { + return tc.frostSortitionPool.IsLocked() + } + return tc.sortitionPool.IsLocked() } // IsOperatorInPool returns true if the operator is registered in // the sortition pool. func (tc *TbtcChain) IsOperatorInPool() (bool, error) { + if tc.hasFrostAuthorization() { + return tc.frostWalletRegistry.IsOperatorInPool( + &bind.CallOpts{From: tc.key.Address}, + tc.key.Address, + ) + } + return tc.walletRegistry.IsOperatorInPool(tc.key.Address) } @@ -373,12 +666,28 @@ func (tc *TbtcChain) IsOperatorInPool() (bool, error) { // If the operator is not in the sortition pool and their authorized stake // is non-zero, function returns false. func (tc *TbtcChain) IsOperatorUpToDate() (bool, error) { + if tc.hasFrostAuthorization() { + return tc.frostWalletRegistry.IsOperatorUpToDate( + &bind.CallOpts{From: tc.key.Address}, + tc.key.Address, + ) + } + return tc.walletRegistry.IsOperatorUpToDate(tc.key.Address) } // JoinSortitionPool executes a transaction to have the operator join the // sortition pool. func (tc *TbtcChain) JoinSortitionPool() error { + if tc.hasFrostAuthorization() { + return tc.submitFrostWalletRegistryTransaction( + "joinSortitionPool", + func(opts *bind.TransactOpts) (*types.Transaction, error) { + return tc.frostWalletRegistry.JoinSortitionPool(opts) + }, + ) + } + _, err := tc.walletRegistry.JoinSortitionPool() return err } @@ -386,6 +695,18 @@ func (tc *TbtcChain) JoinSortitionPool() error { // UpdateOperatorStatus executes a transaction to update the operator's // state in the sortition pool. func (tc *TbtcChain) UpdateOperatorStatus() error { + if tc.hasFrostAuthorization() { + return tc.submitFrostWalletRegistryTransaction( + "updateOperatorStatus", + func(opts *bind.TransactOpts) (*types.Transaction, error) { + return tc.frostWalletRegistry.UpdateOperatorStatus( + opts, + tc.key.Address, + ) + }, + ) + } + _, err := tc.walletRegistry.UpdateOperatorStatus(tc.key.Address) return err } @@ -393,42 +714,82 @@ func (tc *TbtcChain) UpdateOperatorStatus() error { // IsEligibleForRewards checks whether the operator is eligible for rewards // or not. func (tc *TbtcChain) IsEligibleForRewards() (bool, error) { + if tc.hasFrostAuthorization() { + return tc.frostSortitionPool.IsEligibleForRewards(tc.key.Address) + } + return tc.sortitionPool.IsEligibleForRewards(tc.key.Address) } // Checks whether the operator is able to restore their eligibility for // rewards right away. func (tc *TbtcChain) CanRestoreRewardEligibility() (bool, error) { + if tc.hasFrostAuthorization() { + return tc.frostSortitionPool.CanRestoreRewardEligibility(tc.key.Address) + } + return tc.sortitionPool.CanRestoreRewardEligibility(tc.key.Address) } // Restores reward eligibility for the operator. func (tc *TbtcChain) RestoreRewardEligibility() error { + if tc.hasFrostAuthorization() { + _, err := tc.frostSortitionPool.RestoreRewardEligibility(tc.key.Address) + return err + } + _, err := tc.sortitionPool.RestoreRewardEligibility(tc.key.Address) return err } // Returns true if the chaosnet phase is active, false otherwise. func (tc *TbtcChain) IsChaosnetActive() (bool, error) { + if tc.hasFrostAuthorization() { + return tc.frostSortitionPool.IsChaosnetActive() + } + return tc.sortitionPool.IsChaosnetActive() } // Returns true if operator is a beta operator, false otherwise. // Chaosnet status does not matter. func (tc *TbtcChain) IsBetaOperator() (bool, error) { + if tc.hasFrostAuthorization() { + return tc.frostSortitionPool.IsBetaOperator(tc.key.Address) + } + return tc.sortitionPool.IsBetaOperator(tc.key.Address) } -// GetOperatorID returns the ID number of the given operator address. An ID -// number of 0 means the operator has not been allocated an ID number yet. +// GetOperatorID returns the legacy ECDSA sortition pool ID number of the given +// operator address. An ID number of 0 means the operator has not been allocated +// an ID number yet. +// +// This method intentionally remains bound to the legacy ECDSA sortition pool +// even when FROST authorization is configured. Existing ECDSA tBTC flows such +// as DKG approval, inactivity claims, and tbtcpg moving-funds claims compare +// against ECDSA WalletRegistry member IDs. FROST DKG paths use +// SelectFrostGroup and the FROST sortition pool directly. func (tc *TbtcChain) GetOperatorID( operatorAddress chain.Address, ) (chain.OperatorID, error) { - return tc.sortitionPool.GetOperatorID( + return getOperatorID( + tc.sortitionPool, common.HexToAddress(operatorAddress.String()), ) } +type operatorIDResolver interface { + GetOperatorID(operator common.Address) (chain.OperatorID, error) +} + +func getOperatorID( + sortitionPool operatorIDResolver, + operatorAddress common.Address, +) (chain.OperatorID, error) { + return sortitionPool.GetOperatorID(operatorAddress) +} + // SelectGroup returns the group members selected for the current group // selection. The function returns an error if the chain's state does not allow // for group selection at the moment. @@ -1251,6 +1612,75 @@ func (tc *TbtcChain) PastDepositRevealedEvents( return convertedEvents, err } +func (tc *TbtcChain) PastTaprootDepositRevealedEvents( + filter *tbtc.DepositRevealedEventFilter, +) ([]*tbtc.TaprootDepositRevealedEvent, error) { + var startBlock uint64 + var endBlock *uint64 + var depositor []common.Address + var walletPublicKeyHash [][20]byte + + if filter != nil { + startBlock = filter.StartBlock + endBlock = filter.EndBlock + + for _, d := range filter.Depositor { + depositor = append(depositor, common.HexToAddress(d.String())) + } + + walletPublicKeyHash = filter.WalletPublicKeyHash + } + + events, err := tc.bridge.PastTaprootDepositRevealedEvents( + startBlock, + endBlock, + depositor, + walletPublicKeyHash, + ) + if err != nil { + return nil, err + } + + convertedEvents := make([]*tbtc.TaprootDepositRevealedEvent, 0) + for _, event := range events { + var vault *chain.Address + if event.Vault != [20]byte{} { + v := chain.Address(event.Vault.Hex()) + vault = &v + } + + convertedEvent := &tbtc.TaprootDepositRevealedEvent{ + // We can map the event.FundingTxHash field directly to the + // bitcoin.Hash type. This is because event.FundingTxHash is + // a [32]byte type representing a hash in the bitcoin.InternalByteOrder, + // just as bitcoin.Hash assumes. + FundingTxHash: event.FundingTxHash, + FundingOutputIndex: event.FundingOutputIndex, + Depositor: chain.Address(event.Depositor.Hex()), + Amount: event.Amount, + BlindingFactor: event.BlindingFactor, + WalletPublicKeyHash: event.WalletPubKeyHash, + WalletXOnlyPublicKey: event.WalletXOnlyPublicKey, + RefundPublicKeyHash: event.RefundPubKeyHash, + RefundXOnlyPublicKey: event.RefundXOnlyPublicKey, + RefundLocktime: event.RefundLocktime, + Vault: vault, + BlockNumber: event.Raw.BlockNumber, + } + + convertedEvents = append(convertedEvents, convertedEvent) + } + + sort.SliceStable( + convertedEvents, + func(i, j int) bool { + return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber + }, + ) + + return convertedEvents, err +} + func (tc *TbtcChain) PastRedemptionRequestedEvents( filter *tbtc.RedemptionRequestedEventFilter, ) ([]*tbtc.RedemptionRequestedEvent, error) { @@ -1374,35 +1804,79 @@ 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( + return pastNewWalletRegisteredEvents( startBlock, endBlock, + walletID, ecdsaWalletID, walletPublicKeyHash, + tc.bridge, + tc.bridge.PastNewWalletRegisteredEvents, + ) +} + +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, + bridge any, + pastLegacyEvents pastNewWalletRegisteredEventsFn, +) ([]*tbtc.NewWalletRegisteredEvent, error) { + convertedEvents, err := pastNewWalletRegisteredV2Events( + startBlock, + endBlock, + walletID, + ecdsaWalletID, + walletPublicKeyHash, + bridge, ) if err != nil { return nil, err } - convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0) - for _, event := range events { - convertedEvent := &tbtc.NewWalletRegisteredEvent{ - EcdsaWalletID: event.EcdsaWalletID, - WalletPublicKeyHash: event.WalletPubKeyHash, - BlockNumber: event.Raw.BlockNumber, + // Fallback for legacy deployments that do not emit NewWalletRegisteredV2. + if len(convertedEvents) == 0 && len(walletID) == 0 { + legacyEvents, err := pastLegacyEvents( + startBlock, + endBlock, + ecdsaWalletID, + walletPublicKeyHash, + ) + if err != nil { + return nil, err } - convertedEvents = append(convertedEvents, convertedEvent) + 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( @@ -1412,7 +1886,160 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( }, ) - return convertedEvents, err + return convertedEvents, nil +} + +func pastNewWalletRegisteredV2Events( + startBlock uint64, + endBlock *uint64, + walletID [][32]byte, + ecdsaWalletID [][32]byte, + 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 + } + + 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]", + 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.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") + } + + 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() || + 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( @@ -1448,6 +2075,26 @@ func (tc *TbtcChain) IsWalletRegistered(EcdsaWalletID [32]byte) (bool, error) { return isWalletRegistered, nil } +func (tc *TbtcChain) IsFrostWalletRegistered(walletID [32]byte) (bool, error) { + if tc.frostWalletRegistry == nil { + return false, fmt.Errorf("FROST wallet registry is not configured") + } + + isWalletRegistered, err := tc.frostWalletRegistry.IsWalletRegistered( + &bind.CallOpts{}, + walletID, + ) + if err != nil { + return false, fmt.Errorf( + "cannot check if FROST wallet with ID [0x%x] is registered: [%v]", + walletID, + err, + ) + } + + return isWalletRegistered, nil +} + func (tc *TbtcChain) GetWallet( walletPublicKeyHash [20]byte, ) (*tbtc.WalletChainData, error) { @@ -1473,7 +2120,17 @@ func (tc *TbtcChain) GetWallet( return nil, fmt.Errorf("cannot parse wallet state: [%v]", err) } + walletID, err := walletIDForWalletPublicKeyHash( + tc.bridge, + walletPublicKeyHash, + ) + if err != nil { + // Fallback for legacy deployments where walletID accessor may not exist. + walletID = tbtc.DeriveLegacyWalletID(walletPublicKeyHash) + } + return &tbtc.WalletChainData{ + WalletID: walletID, EcdsaWalletID: wallet.EcdsaWalletID, MainUtxoHash: wallet.MainUtxoHash, PendingRedemptionsValue: wallet.PendingRedemptionsValue, @@ -1486,6 +2143,82 @@ func (tc *TbtcChain) GetWallet( }, nil } +func (tc *TbtcChain) WalletPublicKeyHashForWalletID( + walletID [32]byte, +) ([20]byte, error) { + return resolveWalletPublicKeyHashForWalletID( + walletID, + tc.bridge, + ) +} + +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, + bridge any, +) ([20]byte, error) { + resolveCanonical, ok := bridge.(walletPublicKeyHashForWalletIDFn) + + var walletPublicKeyHash [20]byte + var err error + if ok { + 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 + } + } + + 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 + } + + 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 { @@ -1994,6 +2727,58 @@ func (tc *TbtcChain) ValidateDepositSweepProposal( return nil } +func (tc *TbtcChain) ValidateTaprootDepositSweepProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.DepositSweepProposal, + depositsExtraInfo []struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, +) error { + dei := make( + []tbtcabi.WalletProposalValidatorTaprootDepositExtraInfo, + len(depositsExtraInfo), + ) + for i, depositExtraInfo := range depositsExtraInfo { + fundingTx := tbtcabi.BitcoinTxInfo2{ + Version: depositExtraInfo.FundingTx.SerializeVersion(), + InputVector: depositExtraInfo.FundingTx.SerializeInputs(), + OutputVector: depositExtraInfo.FundingTx.SerializeOutputs(), + Locktime: depositExtraInfo.FundingTx.SerializeLocktime(), + } + + if !depositExtraInfo.Deposit.IsTaproot() { + return fmt.Errorf("deposit extra info [%v] is not Taproot-native", i) + } + + dei[i] = tbtcabi.WalletProposalValidatorTaprootDepositExtraInfo{ + FundingTx: fundingTx, + BlindingFactor: depositExtraInfo.Deposit.BlindingFactor, + WalletPubKeyHash: depositExtraInfo.Deposit.WalletPublicKeyHash, + WalletXOnlyPublicKey: *depositExtraInfo.Deposit.WalletXOnlyPublicKey, + RefundPubKeyHash: depositExtraInfo.Deposit.RefundPublicKeyHash, + RefundXOnlyPublicKey: *depositExtraInfo.Deposit.RefundXOnlyPublicKey, + RefundLocktime: depositExtraInfo.Deposit.RefundLocktime, + } + } + + valid, err := tc.walletProposalValidator.ValidateTaprootDepositSweepProposal( + convertDepositSweepProposalToAbiType(walletPublicKeyHash, proposal), + dei, + ) + if err != nil { + return fmt.Errorf("validation failed: [%v]", err) + } + + // Should never happen because `validateTaprootDepositSweepProposal` + // returns true or reverts (returns an error) but do the check just in case. + if !valid { + return fmt.Errorf("unexpected validation result") + } + + return nil +} + func (tc *TbtcChain) GetDepositSweepMaxSize() (uint16, error) { return tc.walletProposalValidator.DEPOSITSWEEPMAXSIZE() } @@ -2411,3 +3196,12 @@ func (tc *TbtcChain) GetRedemptionDelay( func (tc *TbtcChain) GetDepositMinAge() (uint32, error) { return tc.walletProposalValidator.DEPOSITMINAGE() } + +func (tc *TbtcChain) CurrentBlockTimestamp() (time.Time, error) { + currentBlock, err := tc.currentBlockHeader() + if err != nil { + return time.Time{}, fmt.Errorf("cannot get current block: [%v]", err) + } + + return time.Unix(int64(currentBlock.Time), 0), nil +} diff --git a/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go b/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go index e76e6f779f..60d00d2955 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/Bridge.go @@ -46,13 +46,6 @@ type BitcoinTxProof struct { CoinbaseProof []byte } -// BitcoinTxRSVSignature is an auto generated low-level Go binding around an user-defined struct. -type BitcoinTxRSVSignature struct { - R [32]byte - S [32]byte - V uint8 -} - // BitcoinTxUTXO is an auto generated low-level Go binding around an user-defined struct. type BitcoinTxUTXO struct { TxHash [32]byte @@ -81,12 +74,16 @@ type DepositDepositRevealInfo struct { Vault common.Address } -// FraudFraudChallenge is an auto generated low-level Go binding around an user-defined struct. -type FraudFraudChallenge struct { - Challenger common.Address - DepositAmount *big.Int - ReportedAt uint32 - Resolved bool +// DepositTaprootDepositRevealInfo is an auto generated low-level Go binding around an user-defined struct. +type DepositTaprootDepositRevealInfo struct { + FundingOutputIndex uint32 + BlindingFactor [8]byte + WalletPubKeyHash [20]byte + WalletXOnlyPublicKey [32]byte + RefundPubKeyHash [20]byte + RefundXOnlyPublicKey [32]byte + RefundLocktime [4]byte + Vault common.Address } // MovingFundsMovedFundsSweepRequest is an auto generated low-level Go binding around an user-defined struct. @@ -121,7 +118,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\"},{\"inputs\":[],\"name\":\"AddressIsZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EcdsaFraudRouterAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"EcdsaFraudRouterAlreadySet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FrostWalletRegistryAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FrostWalletRegistryAlreadySet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LifecycleRouterAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"LifecycleRouterAlreadySet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"MigrateLegacyFraudChallengesNotImplemented\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"P2TRFraudRouterAddressZero\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"P2TRFraudRouterAlreadySet\",\"type\":\"error\"},{\"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\":false,\"internalType\":\"address\",\"name\":\"ecdsaFraudRouter\",\"type\":\"address\"}],\"name\":\"EcdsaFraudRouterSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[],\"name\":\"EcdsaRetired\",\"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\":\"frostWalletRegistry\",\"type\":\"address\"}],\"name\":\"FrostWalletRegistrySet\",\"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\":\"uint8\",\"name\":\"routerKind\",\"type\":\"uint8\"},{\"indexed\":true,\"internalType\":\"uint256\",\"name\":\"challengeKey\",\"type\":\"uint256\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"depositAmount\",\"type\":\"uint256\"}],\"name\":\"LegacyFraudChallengeMigrated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"lifecycleRouter\",\"type\":\"address\"}],\"name\":\"LifecycleRouterSet\",\"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\":\"walletID\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"}],\"name\":\"NewFrostWalletRegistered\",\"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\":true,\"internalType\":\"enumBridgeState.WalletScheme\",\"name\":\"scheme\",\"type\":\"uint8\"}],\"name\":\"NewWalletSchemeSet\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"p2trFraudRouter\",\"type\":\"address\"}],\"name\":\"P2TRFraudRouterSet\",\"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\":\"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\":\"bytes32\",\"name\":\"walletXOnlyPublicKey\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"refundXOnlyPublicKey\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"name\":\"TaprootDepositRevealed\",\"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\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyX\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"publicKeyY\",\"type\":\"bytes32\"}],\"name\":\"__ecdsaWalletHeartbeatFailedCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"}],\"name\":\"__frostWalletCreatedCallback\",\"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\":[],\"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\":[],\"name\":\"ecdsaFraudRouter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"ecdsaRetired\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"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\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"}],\"name\":\"frostLifecycleContext\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"frostRegistry\",\"type\":\"address\"},{\"internalType\":\"bytes32\",\"name\":\"walletID\",\"type\":\"bytes32\"}],\"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\":\"uint8\",\"name\":\"routerKind\",\"type\":\"uint8\"},{\"internalType\":\"uint256[]\",\"name\":\"challengeKeys\",\"type\":\"uint256[]\"}],\"name\":\"migrateLegacyFraudChallenges\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"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\":\"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\":[],\"name\":\"p2trFraudRouter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"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\":[],\"name\":\"retireEcdsa\",\"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\":[{\"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\":\"bytes32\",\"name\":\"walletXOnlyPublicKey\",\"type\":\"bytes32\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes32\",\"name\":\"refundXOnlyPublicKey\",\"type\":\"bytes32\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.TaprootDepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"}],\"name\":\"revealTaprootDeposit\",\"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\":\"bytes32\",\"name\":\"walletXOnlyPublicKey\",\"type\":\"bytes32\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes32\",\"name\":\"refundXOnlyPublicKey\",\"type\":\"bytes32\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"},{\"internalType\":\"address\",\"name\":\"vault\",\"type\":\"address\"}],\"internalType\":\"structDeposit.TaprootDepositRevealInfo\",\"name\":\"reveal\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"extraData\",\"type\":\"bytes32\"}],\"name\":\"revealTaprootDepositWithExtraData\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"ecdsaFraudRouter\",\"type\":\"address\"}],\"name\":\"setEcdsaFraudRouter\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"frostWalletRegistry\",\"type\":\"address\"}],\"name\":\"setFrostWalletRegistry\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"lifecycleRouter\",\"type\":\"address\"}],\"name\":\"setLifecycleRouter\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"p2trFraudRouter\",\"type\":\"address\"}],\"name\":\"setP2TRFraudRouter\",\"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\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"}],\"name\":\"slashWalletForFraud\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"uint32[]\",\"name\":\"walletMembersIDs\",\"type\":\"uint32[]\"},{\"internalType\":\"address\",\"name\":\"challenger\",\"type\":\"address\"}],\"name\":\"slashWalletForP2TRFraud\",\"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\":[{\"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\":\"view\",\"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 +267,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) @@ -442,35 +470,66 @@ func (_Bridge *BridgeCallerSession) Deposits(depositKey *big.Int) (DepositDeposi return _Bridge.Contract.Deposits(&_Bridge.CallOpts, depositKey) } -// FraudChallenges is a free data retrieval call binding the contract method 0x33e957cb. +// EcdsaFraudRouter is a free data retrieval call binding the contract method 0x9fa00083. +// +// Solidity: function ecdsaFraudRouter() view returns(address) +func (_Bridge *BridgeCaller) EcdsaFraudRouter(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "ecdsaFraudRouter") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// EcdsaFraudRouter is a free data retrieval call binding the contract method 0x9fa00083. +// +// Solidity: function ecdsaFraudRouter() view returns(address) +func (_Bridge *BridgeSession) EcdsaFraudRouter() (common.Address, error) { + return _Bridge.Contract.EcdsaFraudRouter(&_Bridge.CallOpts) +} + +// EcdsaFraudRouter is a free data retrieval call binding the contract method 0x9fa00083. +// +// Solidity: function ecdsaFraudRouter() view returns(address) +func (_Bridge *BridgeCallerSession) EcdsaFraudRouter() (common.Address, error) { + return _Bridge.Contract.EcdsaFraudRouter(&_Bridge.CallOpts) +} + +// EcdsaRetired is a free data retrieval call binding the contract method 0xea3257e3. // -// Solidity: function fraudChallenges(uint256 challengeKey) view returns((address,uint256,uint32,bool)) -func (_Bridge *BridgeCaller) FraudChallenges(opts *bind.CallOpts, challengeKey *big.Int) (FraudFraudChallenge, error) { +// Solidity: function ecdsaRetired() view returns(bool) +func (_Bridge *BridgeCaller) EcdsaRetired(opts *bind.CallOpts) (bool, error) { var out []interface{} - err := _Bridge.contract.Call(opts, &out, "fraudChallenges", challengeKey) + err := _Bridge.contract.Call(opts, &out, "ecdsaRetired") if err != nil { - return *new(FraudFraudChallenge), err + return *new(bool), err } - out0 := *abi.ConvertType(out[0], new(FraudFraudChallenge)).(*FraudFraudChallenge) + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) return out0, err } -// FraudChallenges is a free data retrieval call binding the contract method 0x33e957cb. +// EcdsaRetired is a free data retrieval call binding the contract method 0xea3257e3. // -// Solidity: function fraudChallenges(uint256 challengeKey) view returns((address,uint256,uint32,bool)) -func (_Bridge *BridgeSession) FraudChallenges(challengeKey *big.Int) (FraudFraudChallenge, error) { - return _Bridge.Contract.FraudChallenges(&_Bridge.CallOpts, challengeKey) +// Solidity: function ecdsaRetired() view returns(bool) +func (_Bridge *BridgeSession) EcdsaRetired() (bool, error) { + return _Bridge.Contract.EcdsaRetired(&_Bridge.CallOpts) } -// FraudChallenges is a free data retrieval call binding the contract method 0x33e957cb. +// EcdsaRetired is a free data retrieval call binding the contract method 0xea3257e3. // -// Solidity: function fraudChallenges(uint256 challengeKey) view returns((address,uint256,uint32,bool)) -func (_Bridge *BridgeCallerSession) FraudChallenges(challengeKey *big.Int) (FraudFraudChallenge, error) { - return _Bridge.Contract.FraudChallenges(&_Bridge.CallOpts, challengeKey) +// Solidity: function ecdsaRetired() view returns(bool) +func (_Bridge *BridgeCallerSession) EcdsaRetired() (bool, error) { + return _Bridge.Contract.EcdsaRetired(&_Bridge.CallOpts) } // FraudParameters is a free data retrieval call binding the contract method 0x75b922d1. @@ -528,6 +587,82 @@ func (_Bridge *BridgeCallerSession) FraudParameters() (struct { return _Bridge.Contract.FraudParameters(&_Bridge.CallOpts) } +// FrostLifecycleContext is a free data retrieval call binding the contract method 0xd0ebc637. +// +// Solidity: function frostLifecycleContext(bytes20 walletPubKeyHash) view returns(address frostRegistry, bytes32 walletID) +func (_Bridge *BridgeCaller) FrostLifecycleContext(opts *bind.CallOpts, walletPubKeyHash [20]byte) (struct { + FrostRegistry common.Address + WalletID [32]byte +}, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "frostLifecycleContext", walletPubKeyHash) + + outstruct := new(struct { + FrostRegistry common.Address + WalletID [32]byte + }) + if err != nil { + return *outstruct, err + } + + outstruct.FrostRegistry = *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + outstruct.WalletID = *abi.ConvertType(out[1], new([32]byte)).(*[32]byte) + + return *outstruct, err + +} + +// FrostLifecycleContext is a free data retrieval call binding the contract method 0xd0ebc637. +// +// Solidity: function frostLifecycleContext(bytes20 walletPubKeyHash) view returns(address frostRegistry, bytes32 walletID) +func (_Bridge *BridgeSession) FrostLifecycleContext(walletPubKeyHash [20]byte) (struct { + FrostRegistry common.Address + WalletID [32]byte +}, error) { + return _Bridge.Contract.FrostLifecycleContext(&_Bridge.CallOpts, walletPubKeyHash) +} + +// FrostLifecycleContext is a free data retrieval call binding the contract method 0xd0ebc637. +// +// Solidity: function frostLifecycleContext(bytes20 walletPubKeyHash) view returns(address frostRegistry, bytes32 walletID) +func (_Bridge *BridgeCallerSession) FrostLifecycleContext(walletPubKeyHash [20]byte) (struct { + FrostRegistry common.Address + WalletID [32]byte +}, error) { + return _Bridge.Contract.FrostLifecycleContext(&_Bridge.CallOpts, walletPubKeyHash) +} + +// 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) @@ -773,6 +908,37 @@ func (_Bridge *BridgeCallerSession) MovingFundsParameters() (struct { return _Bridge.Contract.MovingFundsParameters(&_Bridge.CallOpts) } +// P2trFraudRouter is a free data retrieval call binding the contract method 0xe3973b03. +// +// Solidity: function p2trFraudRouter() view returns(address) +func (_Bridge *BridgeCaller) P2trFraudRouter(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Bridge.contract.Call(opts, &out, "p2trFraudRouter") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// P2trFraudRouter is a free data retrieval call binding the contract method 0xe3973b03. +// +// Solidity: function p2trFraudRouter() view returns(address) +func (_Bridge *BridgeSession) P2trFraudRouter() (common.Address, error) { + return _Bridge.Contract.P2trFraudRouter(&_Bridge.CallOpts) +} + +// P2trFraudRouter is a free data retrieval call binding the contract method 0xe3973b03. +// +// Solidity: function p2trFraudRouter() view returns(address) +func (_Bridge *BridgeCallerSession) P2trFraudRouter() (common.Address, error) { + return _Bridge.Contract.P2trFraudRouter(&_Bridge.CallOpts) +} + // PendingRedemptions is a free data retrieval call binding the contract method 0x03d952f7. // // Solidity: function pendingRedemptions(uint256 redemptionKey) view returns((address,uint64,uint64,uint64,uint32)) @@ -998,6 +1164,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) view 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) view 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) view 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 +1265,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,25 +1327,35 @@ func (_Bridge *BridgeCallerSession) Wallets(walletPubKeyHash [20]byte) (WalletsW return _Bridge.Contract.Wallets(&_Bridge.CallOpts, walletPubKeyHash) } -// EcdsaWalletCreatedCallback is a paid mutator transaction binding the contract method 0xa8fa0f42. +// WalletsByWalletID is a free data retrieval call binding the contract method 0xa9b2f9a3. // -// Solidity: function __ecdsaWalletCreatedCallback(bytes32 ecdsaWalletID, bytes32 publicKeyX, bytes32 publicKeyY) returns() -func (_Bridge *BridgeTransactor) EcdsaWalletCreatedCallback(opts *bind.TransactOpts, ecdsaWalletID [32]byte, publicKeyX [32]byte, publicKeyY [32]byte) (*types.Transaction, error) { - return _Bridge.contract.Transact(opts, "__ecdsaWalletCreatedCallback", ecdsaWalletID, publicKeyX, publicKeyY) +// 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 + } -// EcdsaWalletCreatedCallback is a paid mutator transaction binding the contract method 0xa8fa0f42. +// WalletsByWalletID is a free data retrieval call binding the contract method 0xa9b2f9a3. // -// Solidity: function __ecdsaWalletCreatedCallback(bytes32 ecdsaWalletID, bytes32 publicKeyX, bytes32 publicKeyY) returns() -func (_Bridge *BridgeSession) EcdsaWalletCreatedCallback(ecdsaWalletID [32]byte, publicKeyX [32]byte, publicKeyY [32]byte) (*types.Transaction, error) { - return _Bridge.Contract.EcdsaWalletCreatedCallback(&_Bridge.TransactOpts, ecdsaWalletID, publicKeyX, publicKeyY) +// 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) } -// EcdsaWalletCreatedCallback is a paid mutator transaction binding the contract method 0xa8fa0f42. +// WalletsByWalletID is a free data retrieval call binding the contract method 0xa9b2f9a3. // -// Solidity: function __ecdsaWalletCreatedCallback(bytes32 ecdsaWalletID, bytes32 publicKeyX, bytes32 publicKeyY) returns() -func (_Bridge *BridgeTransactorSession) EcdsaWalletCreatedCallback(ecdsaWalletID [32]byte, publicKeyX [32]byte, publicKeyY [32]byte) (*types.Transaction, error) { - return _Bridge.Contract.EcdsaWalletCreatedCallback(&_Bridge.TransactOpts, ecdsaWalletID, publicKeyX, publicKeyY) +// 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) } // EcdsaWalletHeartbeatFailedCallback is a paid mutator transaction binding the contract method 0x3dce9812. @@ -1141,46 +1379,25 @@ func (_Bridge *BridgeTransactorSession) EcdsaWalletHeartbeatFailedCallback(arg0 return _Bridge.Contract.EcdsaWalletHeartbeatFailedCallback(&_Bridge.TransactOpts, arg0, publicKeyX, publicKeyY) } -// DefeatFraudChallenge is a paid mutator transaction binding the contract method 0x77145f21. -// -// Solidity: function defeatFraudChallenge(bytes walletPublicKey, bytes preimage, bool witness) returns() -func (_Bridge *BridgeTransactor) DefeatFraudChallenge(opts *bind.TransactOpts, walletPublicKey []byte, preimage []byte, witness bool) (*types.Transaction, error) { - return _Bridge.contract.Transact(opts, "defeatFraudChallenge", walletPublicKey, preimage, witness) -} - -// DefeatFraudChallenge is a paid mutator transaction binding the contract method 0x77145f21. -// -// Solidity: function defeatFraudChallenge(bytes walletPublicKey, bytes preimage, bool witness) returns() -func (_Bridge *BridgeSession) DefeatFraudChallenge(walletPublicKey []byte, preimage []byte, witness bool) (*types.Transaction, error) { - return _Bridge.Contract.DefeatFraudChallenge(&_Bridge.TransactOpts, walletPublicKey, preimage, witness) -} - -// DefeatFraudChallenge is a paid mutator transaction binding the contract method 0x77145f21. +// FrostWalletCreatedCallback is a paid mutator transaction binding the contract method 0xd81c729e. // -// Solidity: function defeatFraudChallenge(bytes walletPublicKey, bytes preimage, bool witness) returns() -func (_Bridge *BridgeTransactorSession) DefeatFraudChallenge(walletPublicKey []byte, preimage []byte, witness bool) (*types.Transaction, error) { - return _Bridge.Contract.DefeatFraudChallenge(&_Bridge.TransactOpts, walletPublicKey, preimage, witness) +// Solidity: function __frostWalletCreatedCallback(bytes32 xOnlyOutputKey) returns() +func (_Bridge *BridgeTransactor) FrostWalletCreatedCallback(opts *bind.TransactOpts, xOnlyOutputKey [32]byte) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "__frostWalletCreatedCallback", xOnlyOutputKey) } -// DefeatFraudChallengeWithHeartbeat is a paid mutator transaction binding the contract method 0x0674f266. +// FrostWalletCreatedCallback is a paid mutator transaction binding the contract method 0xd81c729e. // -// Solidity: function defeatFraudChallengeWithHeartbeat(bytes walletPublicKey, bytes heartbeatMessage) returns() -func (_Bridge *BridgeTransactor) DefeatFraudChallengeWithHeartbeat(opts *bind.TransactOpts, walletPublicKey []byte, heartbeatMessage []byte) (*types.Transaction, error) { - return _Bridge.contract.Transact(opts, "defeatFraudChallengeWithHeartbeat", walletPublicKey, heartbeatMessage) +// Solidity: function __frostWalletCreatedCallback(bytes32 xOnlyOutputKey) returns() +func (_Bridge *BridgeSession) FrostWalletCreatedCallback(xOnlyOutputKey [32]byte) (*types.Transaction, error) { + return _Bridge.Contract.FrostWalletCreatedCallback(&_Bridge.TransactOpts, xOnlyOutputKey) } -// DefeatFraudChallengeWithHeartbeat is a paid mutator transaction binding the contract method 0x0674f266. +// FrostWalletCreatedCallback is a paid mutator transaction binding the contract method 0xd81c729e. // -// Solidity: function defeatFraudChallengeWithHeartbeat(bytes walletPublicKey, bytes heartbeatMessage) returns() -func (_Bridge *BridgeSession) DefeatFraudChallengeWithHeartbeat(walletPublicKey []byte, heartbeatMessage []byte) (*types.Transaction, error) { - return _Bridge.Contract.DefeatFraudChallengeWithHeartbeat(&_Bridge.TransactOpts, walletPublicKey, heartbeatMessage) -} - -// DefeatFraudChallengeWithHeartbeat is a paid mutator transaction binding the contract method 0x0674f266. -// -// Solidity: function defeatFraudChallengeWithHeartbeat(bytes walletPublicKey, bytes heartbeatMessage) returns() -func (_Bridge *BridgeTransactorSession) DefeatFraudChallengeWithHeartbeat(walletPublicKey []byte, heartbeatMessage []byte) (*types.Transaction, error) { - return _Bridge.Contract.DefeatFraudChallengeWithHeartbeat(&_Bridge.TransactOpts, walletPublicKey, heartbeatMessage) +// Solidity: function __frostWalletCreatedCallback(bytes32 xOnlyOutputKey) returns() +func (_Bridge *BridgeTransactorSession) FrostWalletCreatedCallback(xOnlyOutputKey [32]byte) (*types.Transaction, error) { + return _Bridge.Contract.FrostWalletCreatedCallback(&_Bridge.TransactOpts, xOnlyOutputKey) } // Initialize is a paid mutator transaction binding the contract method 0xd246ce16. @@ -1204,25 +1421,46 @@ func (_Bridge *BridgeTransactorSession) Initialize(_bank common.Address, _relay return _Bridge.Contract.Initialize(&_Bridge.TransactOpts, _bank, _relay, _treasury, _ecdsaWalletRegistry, _reimbursementPool, _txProofDifficultyFactor) } -// NotifyFraudChallengeDefeatTimeout is a paid mutator transaction binding the contract method 0x79fc4eb3. +// 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) +} + +// MigrateLegacyFraudChallenges is a paid mutator transaction binding the contract method 0xfe491621. // -// Solidity: function notifyFraudChallengeDefeatTimeout(bytes walletPublicKey, uint32[] walletMembersIDs, bytes preimageSha256) returns() -func (_Bridge *BridgeTransactor) NotifyFraudChallengeDefeatTimeout(opts *bind.TransactOpts, walletPublicKey []byte, walletMembersIDs []uint32, preimageSha256 []byte) (*types.Transaction, error) { - return _Bridge.contract.Transact(opts, "notifyFraudChallengeDefeatTimeout", walletPublicKey, walletMembersIDs, preimageSha256) +// Solidity: function migrateLegacyFraudChallenges(uint8 routerKind, uint256[] challengeKeys) returns() +func (_Bridge *BridgeTransactor) MigrateLegacyFraudChallenges(opts *bind.TransactOpts, routerKind uint8, challengeKeys []*big.Int) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "migrateLegacyFraudChallenges", routerKind, challengeKeys) } -// NotifyFraudChallengeDefeatTimeout is a paid mutator transaction binding the contract method 0x79fc4eb3. +// MigrateLegacyFraudChallenges is a paid mutator transaction binding the contract method 0xfe491621. // -// Solidity: function notifyFraudChallengeDefeatTimeout(bytes walletPublicKey, uint32[] walletMembersIDs, bytes preimageSha256) returns() -func (_Bridge *BridgeSession) NotifyFraudChallengeDefeatTimeout(walletPublicKey []byte, walletMembersIDs []uint32, preimageSha256 []byte) (*types.Transaction, error) { - return _Bridge.Contract.NotifyFraudChallengeDefeatTimeout(&_Bridge.TransactOpts, walletPublicKey, walletMembersIDs, preimageSha256) +// Solidity: function migrateLegacyFraudChallenges(uint8 routerKind, uint256[] challengeKeys) returns() +func (_Bridge *BridgeSession) MigrateLegacyFraudChallenges(routerKind uint8, challengeKeys []*big.Int) (*types.Transaction, error) { + return _Bridge.Contract.MigrateLegacyFraudChallenges(&_Bridge.TransactOpts, routerKind, challengeKeys) } -// NotifyFraudChallengeDefeatTimeout is a paid mutator transaction binding the contract method 0x79fc4eb3. +// MigrateLegacyFraudChallenges is a paid mutator transaction binding the contract method 0xfe491621. // -// Solidity: function notifyFraudChallengeDefeatTimeout(bytes walletPublicKey, uint32[] walletMembersIDs, bytes preimageSha256) returns() -func (_Bridge *BridgeTransactorSession) NotifyFraudChallengeDefeatTimeout(walletPublicKey []byte, walletMembersIDs []uint32, preimageSha256 []byte) (*types.Transaction, error) { - return _Bridge.Contract.NotifyFraudChallengeDefeatTimeout(&_Bridge.TransactOpts, walletPublicKey, walletMembersIDs, preimageSha256) +// Solidity: function migrateLegacyFraudChallenges(uint8 routerKind, uint256[] challengeKeys) returns() +func (_Bridge *BridgeTransactorSession) MigrateLegacyFraudChallenges(routerKind uint8, challengeKeys []*big.Int) (*types.Transaction, error) { + return _Bridge.Contract.MigrateLegacyFraudChallenges(&_Bridge.TransactOpts, routerKind, challengeKeys) } // NotifyMovedFundsSweepTimeout is a paid mutator transaction binding the contract method 0x50aea15a. @@ -1456,6 +1694,27 @@ func (_Bridge *BridgeTransactorSession) ResetMovingFundsTimeout(walletPubKeyHash return _Bridge.Contract.ResetMovingFundsTimeout(&_Bridge.TransactOpts, walletPubKeyHash) } +// RetireEcdsa is a paid mutator transaction binding the contract method 0x0652611e. +// +// Solidity: function retireEcdsa() returns() +func (_Bridge *BridgeTransactor) RetireEcdsa(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "retireEcdsa") +} + +// RetireEcdsa is a paid mutator transaction binding the contract method 0x0652611e. +// +// Solidity: function retireEcdsa() returns() +func (_Bridge *BridgeSession) RetireEcdsa() (*types.Transaction, error) { + return _Bridge.Contract.RetireEcdsa(&_Bridge.TransactOpts) +} + +// RetireEcdsa is a paid mutator transaction binding the contract method 0x0652611e. +// +// Solidity: function retireEcdsa() returns() +func (_Bridge *BridgeTransactorSession) RetireEcdsa() (*types.Transaction, error) { + return _Bridge.Contract.RetireEcdsa(&_Bridge.TransactOpts) +} + // RevealDeposit is a paid mutator transaction binding the contract method 0xfca4ba4c. // // Solidity: function revealDeposit((bytes4,bytes,bytes,bytes4) fundingTx, (uint32,bytes8,bytes20,bytes20,bytes4,address) reveal) returns() @@ -1498,6 +1757,153 @@ func (_Bridge *BridgeTransactorSession) RevealDepositWithExtraData(fundingTx Bit return _Bridge.Contract.RevealDepositWithExtraData(&_Bridge.TransactOpts, fundingTx, reveal, extraData) } +// RevealTaprootDeposit is a paid mutator transaction binding the contract method 0xbbbafefa. +// +// Solidity: function revealTaprootDeposit((bytes4,bytes,bytes,bytes4) fundingTx, (uint32,bytes8,bytes20,bytes32,bytes20,bytes32,bytes4,address) reveal) returns() +func (_Bridge *BridgeTransactor) RevealTaprootDeposit(opts *bind.TransactOpts, fundingTx BitcoinTxInfo, reveal DepositTaprootDepositRevealInfo) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "revealTaprootDeposit", fundingTx, reveal) +} + +// RevealTaprootDeposit is a paid mutator transaction binding the contract method 0xbbbafefa. +// +// Solidity: function revealTaprootDeposit((bytes4,bytes,bytes,bytes4) fundingTx, (uint32,bytes8,bytes20,bytes32,bytes20,bytes32,bytes4,address) reveal) returns() +func (_Bridge *BridgeSession) RevealTaprootDeposit(fundingTx BitcoinTxInfo, reveal DepositTaprootDepositRevealInfo) (*types.Transaction, error) { + return _Bridge.Contract.RevealTaprootDeposit(&_Bridge.TransactOpts, fundingTx, reveal) +} + +// RevealTaprootDeposit is a paid mutator transaction binding the contract method 0xbbbafefa. +// +// Solidity: function revealTaprootDeposit((bytes4,bytes,bytes,bytes4) fundingTx, (uint32,bytes8,bytes20,bytes32,bytes20,bytes32,bytes4,address) reveal) returns() +func (_Bridge *BridgeTransactorSession) RevealTaprootDeposit(fundingTx BitcoinTxInfo, reveal DepositTaprootDepositRevealInfo) (*types.Transaction, error) { + return _Bridge.Contract.RevealTaprootDeposit(&_Bridge.TransactOpts, fundingTx, reveal) +} + +// RevealTaprootDepositWithExtraData is a paid mutator transaction binding the contract method 0xa97c9f34. +// +// Solidity: function revealTaprootDepositWithExtraData((bytes4,bytes,bytes,bytes4) fundingTx, (uint32,bytes8,bytes20,bytes32,bytes20,bytes32,bytes4,address) reveal, bytes32 extraData) returns() +func (_Bridge *BridgeTransactor) RevealTaprootDepositWithExtraData(opts *bind.TransactOpts, fundingTx BitcoinTxInfo, reveal DepositTaprootDepositRevealInfo, extraData [32]byte) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "revealTaprootDepositWithExtraData", fundingTx, reveal, extraData) +} + +// RevealTaprootDepositWithExtraData is a paid mutator transaction binding the contract method 0xa97c9f34. +// +// Solidity: function revealTaprootDepositWithExtraData((bytes4,bytes,bytes,bytes4) fundingTx, (uint32,bytes8,bytes20,bytes32,bytes20,bytes32,bytes4,address) reveal, bytes32 extraData) returns() +func (_Bridge *BridgeSession) RevealTaprootDepositWithExtraData(fundingTx BitcoinTxInfo, reveal DepositTaprootDepositRevealInfo, extraData [32]byte) (*types.Transaction, error) { + return _Bridge.Contract.RevealTaprootDepositWithExtraData(&_Bridge.TransactOpts, fundingTx, reveal, extraData) +} + +// RevealTaprootDepositWithExtraData is a paid mutator transaction binding the contract method 0xa97c9f34. +// +// Solidity: function revealTaprootDepositWithExtraData((bytes4,bytes,bytes,bytes4) fundingTx, (uint32,bytes8,bytes20,bytes32,bytes20,bytes32,bytes4,address) reveal, bytes32 extraData) returns() +func (_Bridge *BridgeTransactorSession) RevealTaprootDepositWithExtraData(fundingTx BitcoinTxInfo, reveal DepositTaprootDepositRevealInfo, extraData [32]byte) (*types.Transaction, error) { + return _Bridge.Contract.RevealTaprootDepositWithExtraData(&_Bridge.TransactOpts, fundingTx, reveal, extraData) +} + +// SetEcdsaFraudRouter is a paid mutator transaction binding the contract method 0xba863979. +// +// Solidity: function setEcdsaFraudRouter(address ecdsaFraudRouter) returns() +func (_Bridge *BridgeTransactor) SetEcdsaFraudRouter(opts *bind.TransactOpts, ecdsaFraudRouter common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "setEcdsaFraudRouter", ecdsaFraudRouter) +} + +// SetEcdsaFraudRouter is a paid mutator transaction binding the contract method 0xba863979. +// +// Solidity: function setEcdsaFraudRouter(address ecdsaFraudRouter) returns() +func (_Bridge *BridgeSession) SetEcdsaFraudRouter(ecdsaFraudRouter common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetEcdsaFraudRouter(&_Bridge.TransactOpts, ecdsaFraudRouter) +} + +// SetEcdsaFraudRouter is a paid mutator transaction binding the contract method 0xba863979. +// +// Solidity: function setEcdsaFraudRouter(address ecdsaFraudRouter) returns() +func (_Bridge *BridgeTransactorSession) SetEcdsaFraudRouter(ecdsaFraudRouter common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetEcdsaFraudRouter(&_Bridge.TransactOpts, ecdsaFraudRouter) +} + +// SetFrostWalletRegistry is a paid mutator transaction binding the contract method 0x07fe5dad. +// +// Solidity: function setFrostWalletRegistry(address frostWalletRegistry) returns() +func (_Bridge *BridgeTransactor) SetFrostWalletRegistry(opts *bind.TransactOpts, frostWalletRegistry common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "setFrostWalletRegistry", frostWalletRegistry) +} + +// SetFrostWalletRegistry is a paid mutator transaction binding the contract method 0x07fe5dad. +// +// Solidity: function setFrostWalletRegistry(address frostWalletRegistry) returns() +func (_Bridge *BridgeSession) SetFrostWalletRegistry(frostWalletRegistry common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetFrostWalletRegistry(&_Bridge.TransactOpts, frostWalletRegistry) +} + +// SetFrostWalletRegistry is a paid mutator transaction binding the contract method 0x07fe5dad. +// +// Solidity: function setFrostWalletRegistry(address frostWalletRegistry) returns() +func (_Bridge *BridgeTransactorSession) SetFrostWalletRegistry(frostWalletRegistry common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetFrostWalletRegistry(&_Bridge.TransactOpts, frostWalletRegistry) +} + +// SetLifecycleRouter is a paid mutator transaction binding the contract method 0xdf5efac8. +// +// Solidity: function setLifecycleRouter(address lifecycleRouter) returns() +func (_Bridge *BridgeTransactor) SetLifecycleRouter(opts *bind.TransactOpts, lifecycleRouter common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "setLifecycleRouter", lifecycleRouter) +} + +// SetLifecycleRouter is a paid mutator transaction binding the contract method 0xdf5efac8. +// +// Solidity: function setLifecycleRouter(address lifecycleRouter) returns() +func (_Bridge *BridgeSession) SetLifecycleRouter(lifecycleRouter common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetLifecycleRouter(&_Bridge.TransactOpts, lifecycleRouter) +} + +// SetLifecycleRouter is a paid mutator transaction binding the contract method 0xdf5efac8. +// +// Solidity: function setLifecycleRouter(address lifecycleRouter) returns() +func (_Bridge *BridgeTransactorSession) SetLifecycleRouter(lifecycleRouter common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetLifecycleRouter(&_Bridge.TransactOpts, lifecycleRouter) +} + +// SetP2TRFraudRouter is a paid mutator transaction binding the contract method 0x6247cf16. +// +// Solidity: function setP2TRFraudRouter(address p2trFraudRouter) returns() +func (_Bridge *BridgeTransactor) SetP2TRFraudRouter(opts *bind.TransactOpts, p2trFraudRouter common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "setP2TRFraudRouter", p2trFraudRouter) +} + +// SetP2TRFraudRouter is a paid mutator transaction binding the contract method 0x6247cf16. +// +// Solidity: function setP2TRFraudRouter(address p2trFraudRouter) returns() +func (_Bridge *BridgeSession) SetP2TRFraudRouter(p2trFraudRouter common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetP2TRFraudRouter(&_Bridge.TransactOpts, p2trFraudRouter) +} + +// SetP2TRFraudRouter is a paid mutator transaction binding the contract method 0x6247cf16. +// +// Solidity: function setP2TRFraudRouter(address p2trFraudRouter) returns() +func (_Bridge *BridgeTransactorSession) SetP2TRFraudRouter(p2trFraudRouter common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SetP2TRFraudRouter(&_Bridge.TransactOpts, p2trFraudRouter) +} + +// 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() @@ -1561,52 +1967,73 @@ func (_Bridge *BridgeTransactorSession) SetVaultStatus(vault common.Address, isT return _Bridge.Contract.SetVaultStatus(&_Bridge.TransactOpts, vault, isTrusted) } -// SubmitDepositSweepProof is a paid mutator transaction binding the contract method 0xbd150131. +// SlashWalletForFraud is a paid mutator transaction binding the contract method 0x3f5dfabb. // -// Solidity: function submitDepositSweepProof((bytes4,bytes,bytes,bytes4) sweepTx, (bytes,uint256,bytes,bytes32,bytes) sweepProof, (bytes32,uint32,uint64) mainUtxo, address vault) returns() -func (_Bridge *BridgeTransactor) SubmitDepositSweepProof(opts *bind.TransactOpts, sweepTx BitcoinTxInfo, sweepProof BitcoinTxProof, mainUtxo BitcoinTxUTXO, vault common.Address) (*types.Transaction, error) { - return _Bridge.contract.Transact(opts, "submitDepositSweepProof", sweepTx, sweepProof, mainUtxo, vault) +// Solidity: function slashWalletForFraud(bytes20 walletPubKeyHash, uint32[] walletMembersIDs, address challenger) returns() +func (_Bridge *BridgeTransactor) SlashWalletForFraud(opts *bind.TransactOpts, walletPubKeyHash [20]byte, walletMembersIDs []uint32, challenger common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "slashWalletForFraud", walletPubKeyHash, walletMembersIDs, challenger) } -// SubmitDepositSweepProof is a paid mutator transaction binding the contract method 0xbd150131. +// SlashWalletForFraud is a paid mutator transaction binding the contract method 0x3f5dfabb. // -// Solidity: function submitDepositSweepProof((bytes4,bytes,bytes,bytes4) sweepTx, (bytes,uint256,bytes,bytes32,bytes) sweepProof, (bytes32,uint32,uint64) mainUtxo, address vault) returns() -func (_Bridge *BridgeSession) SubmitDepositSweepProof(sweepTx BitcoinTxInfo, sweepProof BitcoinTxProof, mainUtxo BitcoinTxUTXO, vault common.Address) (*types.Transaction, error) { - return _Bridge.Contract.SubmitDepositSweepProof(&_Bridge.TransactOpts, sweepTx, sweepProof, mainUtxo, vault) +// Solidity: function slashWalletForFraud(bytes20 walletPubKeyHash, uint32[] walletMembersIDs, address challenger) returns() +func (_Bridge *BridgeSession) SlashWalletForFraud(walletPubKeyHash [20]byte, walletMembersIDs []uint32, challenger common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SlashWalletForFraud(&_Bridge.TransactOpts, walletPubKeyHash, walletMembersIDs, challenger) } -// SubmitDepositSweepProof is a paid mutator transaction binding the contract method 0xbd150131. +// SlashWalletForFraud is a paid mutator transaction binding the contract method 0x3f5dfabb. // -// Solidity: function submitDepositSweepProof((bytes4,bytes,bytes,bytes4) sweepTx, (bytes,uint256,bytes,bytes32,bytes) sweepProof, (bytes32,uint32,uint64) mainUtxo, address vault) returns() -func (_Bridge *BridgeTransactorSession) SubmitDepositSweepProof(sweepTx BitcoinTxInfo, sweepProof BitcoinTxProof, mainUtxo BitcoinTxUTXO, vault common.Address) (*types.Transaction, error) { - return _Bridge.Contract.SubmitDepositSweepProof(&_Bridge.TransactOpts, sweepTx, sweepProof, mainUtxo, vault) +// Solidity: function slashWalletForFraud(bytes20 walletPubKeyHash, uint32[] walletMembersIDs, address challenger) returns() +func (_Bridge *BridgeTransactorSession) SlashWalletForFraud(walletPubKeyHash [20]byte, walletMembersIDs []uint32, challenger common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SlashWalletForFraud(&_Bridge.TransactOpts, walletPubKeyHash, walletMembersIDs, challenger) } -// SubmitFraudChallenge is a paid mutator transaction binding the contract method 0x685ce1b1. +// SlashWalletForP2TRFraud is a paid mutator transaction binding the contract method 0xc823b5cf. // -// Solidity: function submitFraudChallenge(bytes walletPublicKey, bytes preimageSha256, (bytes32,bytes32,uint8) signature) payable returns() -func (_Bridge *BridgeTransactor) SubmitFraudChallenge(opts *bind.TransactOpts, walletPublicKey []byte, preimageSha256 []byte, signature BitcoinTxRSVSignature) (*types.Transaction, error) { - return _Bridge.contract.Transact(opts, "submitFraudChallenge", walletPublicKey, preimageSha256, signature) +// Solidity: function slashWalletForP2TRFraud(bytes20 walletPubKeyHash, uint32[] walletMembersIDs, address challenger) returns() +func (_Bridge *BridgeTransactor) SlashWalletForP2TRFraud(opts *bind.TransactOpts, walletPubKeyHash [20]byte, walletMembersIDs []uint32, challenger common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "slashWalletForP2TRFraud", walletPubKeyHash, walletMembersIDs, challenger) } -// SubmitFraudChallenge is a paid mutator transaction binding the contract method 0x685ce1b1. +// SlashWalletForP2TRFraud is a paid mutator transaction binding the contract method 0xc823b5cf. // -// Solidity: function submitFraudChallenge(bytes walletPublicKey, bytes preimageSha256, (bytes32,bytes32,uint8) signature) payable returns() -func (_Bridge *BridgeSession) SubmitFraudChallenge(walletPublicKey []byte, preimageSha256 []byte, signature BitcoinTxRSVSignature) (*types.Transaction, error) { - return _Bridge.Contract.SubmitFraudChallenge(&_Bridge.TransactOpts, walletPublicKey, preimageSha256, signature) +// Solidity: function slashWalletForP2TRFraud(bytes20 walletPubKeyHash, uint32[] walletMembersIDs, address challenger) returns() +func (_Bridge *BridgeSession) SlashWalletForP2TRFraud(walletPubKeyHash [20]byte, walletMembersIDs []uint32, challenger common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SlashWalletForP2TRFraud(&_Bridge.TransactOpts, walletPubKeyHash, walletMembersIDs, challenger) } -// SubmitFraudChallenge is a paid mutator transaction binding the contract method 0x685ce1b1. +// SlashWalletForP2TRFraud is a paid mutator transaction binding the contract method 0xc823b5cf. // -// Solidity: function submitFraudChallenge(bytes walletPublicKey, bytes preimageSha256, (bytes32,bytes32,uint8) signature) payable returns() -func (_Bridge *BridgeTransactorSession) SubmitFraudChallenge(walletPublicKey []byte, preimageSha256 []byte, signature BitcoinTxRSVSignature) (*types.Transaction, error) { - return _Bridge.Contract.SubmitFraudChallenge(&_Bridge.TransactOpts, walletPublicKey, preimageSha256, signature) +// Solidity: function slashWalletForP2TRFraud(bytes20 walletPubKeyHash, uint32[] walletMembersIDs, address challenger) returns() +func (_Bridge *BridgeTransactorSession) SlashWalletForP2TRFraud(walletPubKeyHash [20]byte, walletMembersIDs []uint32, challenger common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SlashWalletForP2TRFraud(&_Bridge.TransactOpts, walletPubKeyHash, walletMembersIDs, challenger) } -// SubmitMovedFundsSweepProof is a paid mutator transaction binding the contract method 0x9821c38b. +// SubmitDepositSweepProof is a paid mutator transaction binding the contract method 0xbd150131. // -// Solidity: function submitMovedFundsSweepProof((bytes4,bytes,bytes,bytes4) sweepTx, (bytes,uint256,bytes,bytes32,bytes) sweepProof, (bytes32,uint32,uint64) mainUtxo) returns() -func (_Bridge *BridgeTransactor) SubmitMovedFundsSweepProof(opts *bind.TransactOpts, sweepTx BitcoinTxInfo, sweepProof BitcoinTxProof, mainUtxo BitcoinTxUTXO) (*types.Transaction, error) { +// Solidity: function submitDepositSweepProof((bytes4,bytes,bytes,bytes4) sweepTx, (bytes,uint256,bytes,bytes32,bytes) sweepProof, (bytes32,uint32,uint64) mainUtxo, address vault) returns() +func (_Bridge *BridgeTransactor) SubmitDepositSweepProof(opts *bind.TransactOpts, sweepTx BitcoinTxInfo, sweepProof BitcoinTxProof, mainUtxo BitcoinTxUTXO, vault common.Address) (*types.Transaction, error) { + return _Bridge.contract.Transact(opts, "submitDepositSweepProof", sweepTx, sweepProof, mainUtxo, vault) +} + +// SubmitDepositSweepProof is a paid mutator transaction binding the contract method 0xbd150131. +// +// Solidity: function submitDepositSweepProof((bytes4,bytes,bytes,bytes4) sweepTx, (bytes,uint256,bytes,bytes32,bytes) sweepProof, (bytes32,uint32,uint64) mainUtxo, address vault) returns() +func (_Bridge *BridgeSession) SubmitDepositSweepProof(sweepTx BitcoinTxInfo, sweepProof BitcoinTxProof, mainUtxo BitcoinTxUTXO, vault common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SubmitDepositSweepProof(&_Bridge.TransactOpts, sweepTx, sweepProof, mainUtxo, vault) +} + +// SubmitDepositSweepProof is a paid mutator transaction binding the contract method 0xbd150131. +// +// Solidity: function submitDepositSweepProof((bytes4,bytes,bytes,bytes4) sweepTx, (bytes,uint256,bytes,bytes32,bytes) sweepProof, (bytes32,uint32,uint64) mainUtxo, address vault) returns() +func (_Bridge *BridgeTransactorSession) SubmitDepositSweepProof(sweepTx BitcoinTxInfo, sweepProof BitcoinTxProof, mainUtxo BitcoinTxUTXO, vault common.Address) (*types.Transaction, error) { + return _Bridge.Contract.SubmitDepositSweepProof(&_Bridge.TransactOpts, sweepTx, sweepProof, mainUtxo, vault) +} + +// SubmitMovedFundsSweepProof is a paid mutator transaction binding the contract method 0x9821c38b. +// +// Solidity: function submitMovedFundsSweepProof((bytes4,bytes,bytes,bytes4) sweepTx, (bytes,uint256,bytes,bytes32,bytes) sweepProof, (bytes32,uint32,uint64) mainUtxo) returns() +func (_Bridge *BridgeTransactor) SubmitMovedFundsSweepProof(opts *bind.TransactOpts, sweepTx BitcoinTxInfo, sweepProof BitcoinTxProof, mainUtxo BitcoinTxUTXO) (*types.Transaction, error) { return _Bridge.contract.Transact(opts, "submitMovedFundsSweepProof", sweepTx, sweepProof, mainUtxo) } @@ -2133,9 +2560,9 @@ func (_Bridge *BridgeFilterer) ParseDepositRevealed(log types.Log) (*BridgeDepos 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 +// 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 @@ -2149,7 +2576,7 @@ type BridgeDepositsSweptIterator struct { // 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 *BridgeDepositsSweptIterator) Next() bool { +func (it *BridgeDepositVaultFixedIterator) Next() bool { // If the iterator failed, stop iterating if it.fail != nil { return false @@ -2158,7 +2585,7 @@ func (it *BridgeDepositsSweptIterator) Next() bool { if it.done { select { case log := <-it.logs: - it.Event = new(BridgeDepositsSwept) + it.Event = new(BridgeDepositVaultFixed) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -2173,7 +2600,7 @@ func (it *BridgeDepositsSweptIterator) Next() bool { // Iterator still in progress, wait for either a data or an error event select { case log := <-it.logs: - it.Event = new(BridgeDepositsSwept) + it.Event = new(BridgeDepositVaultFixed) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -2189,42 +2616,52 @@ func (it *BridgeDepositsSweptIterator) Next() bool { } // Error returns any retrieval or parsing error occurred during filtering. -func (it *BridgeDepositsSweptIterator) Error() error { +func (it *BridgeDepositVaultFixedIterator) Error() error { return it.fail } // Close terminates the iteration process, releasing any pending underlying // resources. -func (it *BridgeDepositsSweptIterator) Close() error { +func (it *BridgeDepositVaultFixedIterator) Close() error { it.sub.Unsubscribe() return nil } -// BridgeDepositsSwept represents a DepositsSwept event raised by the Bridge contract. -type BridgeDepositsSwept struct { - WalletPubKeyHash [20]byte - SweepTxHash [32]byte - Raw types.Log // Blockchain specific contextual infos +// 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 } -// FilterDepositsSwept is a free log retrieval operation binding the contract event 0xe50ffdcc0a5f2c1ede5c122b9414ffd7b2c6bc870d2d775194049dc30da95e6a. +// FilterDepositVaultFixed is a free log retrieval operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. // -// Solidity: event DepositsSwept(bytes20 walletPubKeyHash, bytes32 sweepTxHash) -func (_Bridge *BridgeFilterer) FilterDepositsSwept(opts *bind.FilterOpts) (*BridgeDepositsSweptIterator, error) { +// Solidity: event DepositVaultFixed(uint256 indexed depositKey, address newVault) +func (_Bridge *BridgeFilterer) FilterDepositVaultFixed(opts *bind.FilterOpts, depositKey []*big.Int) (*BridgeDepositVaultFixedIterator, error) { - logs, sub, err := _Bridge.contract.FilterLogs(opts, "DepositsSwept") + 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 &BridgeDepositsSweptIterator{contract: _Bridge.contract, event: "DepositsSwept", logs: logs, sub: sub}, nil + return &BridgeDepositVaultFixedIterator{contract: _Bridge.contract, event: "DepositVaultFixed", logs: logs, sub: sub}, nil } -// WatchDepositsSwept is a free log subscription operation binding the contract event 0xe50ffdcc0a5f2c1ede5c122b9414ffd7b2c6bc870d2d775194049dc30da95e6a. +// WatchDepositVaultFixed is a free log subscription operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. // -// Solidity: event DepositsSwept(bytes20 walletPubKeyHash, bytes32 sweepTxHash) -func (_Bridge *BridgeFilterer) WatchDepositsSwept(opts *bind.WatchOpts, sink chan<- *BridgeDepositsSwept) (event.Subscription, error) { +// Solidity: event DepositVaultFixed(uint256 indexed depositKey, address newVault) +func (_Bridge *BridgeFilterer) WatchDepositVaultFixed(opts *bind.WatchOpts, sink chan<- *BridgeDepositVaultFixed, depositKey []*big.Int) (event.Subscription, error) { - logs, sub, err := _Bridge.contract.WatchLogs(opts, "DepositsSwept") + 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 } @@ -2234,8 +2671,8 @@ func (_Bridge *BridgeFilterer) WatchDepositsSwept(opts *bind.WatchOpts, sink cha select { case log := <-logs: // New log arrived, parse the event and forward to the user - event := new(BridgeDepositsSwept) - if err := _Bridge.contract.UnpackLog(event, "DepositsSwept", log); err != nil { + event := new(BridgeDepositVaultFixed) + if err := _Bridge.contract.UnpackLog(event, "DepositVaultFixed", log); err != nil { return err } event.Raw = log @@ -2256,21 +2693,21 @@ func (_Bridge *BridgeFilterer) WatchDepositsSwept(opts *bind.WatchOpts, sink cha }), nil } -// ParseDepositsSwept is a log parse operation binding the contract event 0xe50ffdcc0a5f2c1ede5c122b9414ffd7b2c6bc870d2d775194049dc30da95e6a. +// ParseDepositVaultFixed is a log parse operation binding the contract event 0x6851c9da8832e374b52353e89727e1f35bd403bf45bc19c889e416393bd53973. // -// Solidity: event DepositsSwept(bytes20 walletPubKeyHash, bytes32 sweepTxHash) -func (_Bridge *BridgeFilterer) ParseDepositsSwept(log types.Log) (*BridgeDepositsSwept, error) { - event := new(BridgeDepositsSwept) - if err := _Bridge.contract.UnpackLog(event, "DepositsSwept", log); err != nil { +// 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 } -// BridgeFraudChallengeDefeatTimedOutIterator is returned from FilterFraudChallengeDefeatTimedOut and is used to iterate over the raw logs and unpacked data for FraudChallengeDefeatTimedOut events raised by the Bridge contract. -type BridgeFraudChallengeDefeatTimedOutIterator struct { - Event *BridgeFraudChallengeDefeatTimedOut // Event containing the contract specifics and raw log +// 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 contract *bind.BoundContract // Generic contract to use for unpacking event data event string // Event name to use for unpacking event data @@ -2284,7 +2721,7 @@ type BridgeFraudChallengeDefeatTimedOutIterator struct { // 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 *BridgeFraudChallengeDefeatTimedOutIterator) Next() bool { +func (it *BridgeDepositsSweptIterator) Next() bool { // If the iterator failed, stop iterating if it.fail != nil { return false @@ -2293,7 +2730,7 @@ func (it *BridgeFraudChallengeDefeatTimedOutIterator) Next() bool { if it.done { select { case log := <-it.logs: - it.Event = new(BridgeFraudChallengeDefeatTimedOut) + it.Event = new(BridgeDepositsSwept) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -2308,7 +2745,7 @@ func (it *BridgeFraudChallengeDefeatTimedOutIterator) Next() bool { // Iterator still in progress, wait for either a data or an error event select { case log := <-it.logs: - it.Event = new(BridgeFraudChallengeDefeatTimedOut) + it.Event = new(BridgeDepositsSwept) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -2324,52 +2761,42 @@ func (it *BridgeFraudChallengeDefeatTimedOutIterator) Next() bool { } // Error returns any retrieval or parsing error occurred during filtering. -func (it *BridgeFraudChallengeDefeatTimedOutIterator) Error() error { +func (it *BridgeDepositsSweptIterator) Error() error { return it.fail } // Close terminates the iteration process, releasing any pending underlying // resources. -func (it *BridgeFraudChallengeDefeatTimedOutIterator) Close() error { +func (it *BridgeDepositsSweptIterator) Close() error { it.sub.Unsubscribe() return nil } -// BridgeFraudChallengeDefeatTimedOut represents a FraudChallengeDefeatTimedOut event raised by the Bridge contract. -type BridgeFraudChallengeDefeatTimedOut struct { +// BridgeDepositsSwept represents a DepositsSwept event raised by the Bridge contract. +type BridgeDepositsSwept struct { WalletPubKeyHash [20]byte - Sighash [32]byte + SweepTxHash [32]byte Raw types.Log // Blockchain specific contextual infos } -// FilterFraudChallengeDefeatTimedOut is a free log retrieval operation binding the contract event 0x635230b60143449f10a365568e2bd95e3e8aaed03855631722941c2bad634b77. +// FilterDepositsSwept is a free log retrieval operation binding the contract event 0xe50ffdcc0a5f2c1ede5c122b9414ffd7b2c6bc870d2d775194049dc30da95e6a. // -// Solidity: event FraudChallengeDefeatTimedOut(bytes20 indexed walletPubKeyHash, bytes32 sighash) -func (_Bridge *BridgeFilterer) FilterFraudChallengeDefeatTimedOut(opts *bind.FilterOpts, walletPubKeyHash [][20]byte) (*BridgeFraudChallengeDefeatTimedOutIterator, error) { - - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) - } +// Solidity: event DepositsSwept(bytes20 walletPubKeyHash, bytes32 sweepTxHash) +func (_Bridge *BridgeFilterer) FilterDepositsSwept(opts *bind.FilterOpts) (*BridgeDepositsSweptIterator, error) { - logs, sub, err := _Bridge.contract.FilterLogs(opts, "FraudChallengeDefeatTimedOut", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.FilterLogs(opts, "DepositsSwept") if err != nil { return nil, err } - return &BridgeFraudChallengeDefeatTimedOutIterator{contract: _Bridge.contract, event: "FraudChallengeDefeatTimedOut", logs: logs, sub: sub}, nil + return &BridgeDepositsSweptIterator{contract: _Bridge.contract, event: "DepositsSwept", logs: logs, sub: sub}, nil } -// WatchFraudChallengeDefeatTimedOut is a free log subscription operation binding the contract event 0x635230b60143449f10a365568e2bd95e3e8aaed03855631722941c2bad634b77. +// WatchDepositsSwept is a free log subscription operation binding the contract event 0xe50ffdcc0a5f2c1ede5c122b9414ffd7b2c6bc870d2d775194049dc30da95e6a. // -// Solidity: event FraudChallengeDefeatTimedOut(bytes20 indexed walletPubKeyHash, bytes32 sighash) -func (_Bridge *BridgeFilterer) WatchFraudChallengeDefeatTimedOut(opts *bind.WatchOpts, sink chan<- *BridgeFraudChallengeDefeatTimedOut, walletPubKeyHash [][20]byte) (event.Subscription, error) { - - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) - } +// Solidity: event DepositsSwept(bytes20 walletPubKeyHash, bytes32 sweepTxHash) +func (_Bridge *BridgeFilterer) WatchDepositsSwept(opts *bind.WatchOpts, sink chan<- *BridgeDepositsSwept) (event.Subscription, error) { - logs, sub, err := _Bridge.contract.WatchLogs(opts, "FraudChallengeDefeatTimedOut", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.WatchLogs(opts, "DepositsSwept") if err != nil { return nil, err } @@ -2379,8 +2806,8 @@ func (_Bridge *BridgeFilterer) WatchFraudChallengeDefeatTimedOut(opts *bind.Watc select { case log := <-logs: // New log arrived, parse the event and forward to the user - event := new(BridgeFraudChallengeDefeatTimedOut) - if err := _Bridge.contract.UnpackLog(event, "FraudChallengeDefeatTimedOut", log); err != nil { + event := new(BridgeDepositsSwept) + if err := _Bridge.contract.UnpackLog(event, "DepositsSwept", log); err != nil { return err } event.Raw = log @@ -2401,21 +2828,21 @@ func (_Bridge *BridgeFilterer) WatchFraudChallengeDefeatTimedOut(opts *bind.Watc }), nil } -// ParseFraudChallengeDefeatTimedOut is a log parse operation binding the contract event 0x635230b60143449f10a365568e2bd95e3e8aaed03855631722941c2bad634b77. +// ParseDepositsSwept is a log parse operation binding the contract event 0xe50ffdcc0a5f2c1ede5c122b9414ffd7b2c6bc870d2d775194049dc30da95e6a. // -// Solidity: event FraudChallengeDefeatTimedOut(bytes20 indexed walletPubKeyHash, bytes32 sighash) -func (_Bridge *BridgeFilterer) ParseFraudChallengeDefeatTimedOut(log types.Log) (*BridgeFraudChallengeDefeatTimedOut, error) { - event := new(BridgeFraudChallengeDefeatTimedOut) - if err := _Bridge.contract.UnpackLog(event, "FraudChallengeDefeatTimedOut", log); err != nil { +// Solidity: event DepositsSwept(bytes20 walletPubKeyHash, bytes32 sweepTxHash) +func (_Bridge *BridgeFilterer) ParseDepositsSwept(log types.Log) (*BridgeDepositsSwept, error) { + event := new(BridgeDepositsSwept) + if err := _Bridge.contract.UnpackLog(event, "DepositsSwept", log); err != nil { return nil, err } event.Raw = log return event, nil } -// BridgeFraudChallengeDefeatedIterator is returned from FilterFraudChallengeDefeated and is used to iterate over the raw logs and unpacked data for FraudChallengeDefeated events raised by the Bridge contract. -type BridgeFraudChallengeDefeatedIterator struct { - Event *BridgeFraudChallengeDefeated // Event containing the contract specifics and raw log +// BridgeEcdsaFraudRouterSetIterator is returned from FilterEcdsaFraudRouterSet and is used to iterate over the raw logs and unpacked data for EcdsaFraudRouterSet events raised by the Bridge contract. +type BridgeEcdsaFraudRouterSetIterator struct { + Event *BridgeEcdsaFraudRouterSet // 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 @@ -2429,7 +2856,7 @@ type BridgeFraudChallengeDefeatedIterator struct { // 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 *BridgeFraudChallengeDefeatedIterator) Next() bool { +func (it *BridgeEcdsaFraudRouterSetIterator) Next() bool { // If the iterator failed, stop iterating if it.fail != nil { return false @@ -2438,7 +2865,7 @@ func (it *BridgeFraudChallengeDefeatedIterator) Next() bool { if it.done { select { case log := <-it.logs: - it.Event = new(BridgeFraudChallengeDefeated) + it.Event = new(BridgeEcdsaFraudRouterSet) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -2453,7 +2880,7 @@ func (it *BridgeFraudChallengeDefeatedIterator) Next() bool { // Iterator still in progress, wait for either a data or an error event select { case log := <-it.logs: - it.Event = new(BridgeFraudChallengeDefeated) + it.Event = new(BridgeEcdsaFraudRouterSet) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -2469,52 +2896,41 @@ func (it *BridgeFraudChallengeDefeatedIterator) Next() bool { } // Error returns any retrieval or parsing error occurred during filtering. -func (it *BridgeFraudChallengeDefeatedIterator) Error() error { +func (it *BridgeEcdsaFraudRouterSetIterator) Error() error { return it.fail } // Close terminates the iteration process, releasing any pending underlying // resources. -func (it *BridgeFraudChallengeDefeatedIterator) Close() error { +func (it *BridgeEcdsaFraudRouterSetIterator) Close() error { it.sub.Unsubscribe() return nil } -// BridgeFraudChallengeDefeated represents a FraudChallengeDefeated event raised by the Bridge contract. -type BridgeFraudChallengeDefeated struct { - WalletPubKeyHash [20]byte - Sighash [32]byte +// BridgeEcdsaFraudRouterSet represents a EcdsaFraudRouterSet event raised by the Bridge contract. +type BridgeEcdsaFraudRouterSet struct { + EcdsaFraudRouter common.Address Raw types.Log // Blockchain specific contextual infos } -// FilterFraudChallengeDefeated is a free log retrieval operation binding the contract event 0x6ff720470ffad78f316655e2c7fc77a76763c13de0e19ee52149916ba7e44d3b. +// FilterEcdsaFraudRouterSet is a free log retrieval operation binding the contract event 0x74b82ffdaa86ef071c7c5083b76052a32b9d67ead5e1013cba6979a28d1851c1. // -// Solidity: event FraudChallengeDefeated(bytes20 indexed walletPubKeyHash, bytes32 sighash) -func (_Bridge *BridgeFilterer) FilterFraudChallengeDefeated(opts *bind.FilterOpts, walletPubKeyHash [][20]byte) (*BridgeFraudChallengeDefeatedIterator, error) { - - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) - } +// Solidity: event EcdsaFraudRouterSet(address ecdsaFraudRouter) +func (_Bridge *BridgeFilterer) FilterEcdsaFraudRouterSet(opts *bind.FilterOpts) (*BridgeEcdsaFraudRouterSetIterator, error) { - logs, sub, err := _Bridge.contract.FilterLogs(opts, "FraudChallengeDefeated", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.FilterLogs(opts, "EcdsaFraudRouterSet") if err != nil { return nil, err } - return &BridgeFraudChallengeDefeatedIterator{contract: _Bridge.contract, event: "FraudChallengeDefeated", logs: logs, sub: sub}, nil + return &BridgeEcdsaFraudRouterSetIterator{contract: _Bridge.contract, event: "EcdsaFraudRouterSet", logs: logs, sub: sub}, nil } -// WatchFraudChallengeDefeated is a free log subscription operation binding the contract event 0x6ff720470ffad78f316655e2c7fc77a76763c13de0e19ee52149916ba7e44d3b. +// WatchEcdsaFraudRouterSet is a free log subscription operation binding the contract event 0x74b82ffdaa86ef071c7c5083b76052a32b9d67ead5e1013cba6979a28d1851c1. // -// Solidity: event FraudChallengeDefeated(bytes20 indexed walletPubKeyHash, bytes32 sighash) -func (_Bridge *BridgeFilterer) WatchFraudChallengeDefeated(opts *bind.WatchOpts, sink chan<- *BridgeFraudChallengeDefeated, walletPubKeyHash [][20]byte) (event.Subscription, error) { - - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) - } +// Solidity: event EcdsaFraudRouterSet(address ecdsaFraudRouter) +func (_Bridge *BridgeFilterer) WatchEcdsaFraudRouterSet(opts *bind.WatchOpts, sink chan<- *BridgeEcdsaFraudRouterSet) (event.Subscription, error) { - logs, sub, err := _Bridge.contract.WatchLogs(opts, "FraudChallengeDefeated", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.WatchLogs(opts, "EcdsaFraudRouterSet") if err != nil { return nil, err } @@ -2524,8 +2940,8 @@ func (_Bridge *BridgeFilterer) WatchFraudChallengeDefeated(opts *bind.WatchOpts, select { case log := <-logs: // New log arrived, parse the event and forward to the user - event := new(BridgeFraudChallengeDefeated) - if err := _Bridge.contract.UnpackLog(event, "FraudChallengeDefeated", log); err != nil { + event := new(BridgeEcdsaFraudRouterSet) + if err := _Bridge.contract.UnpackLog(event, "EcdsaFraudRouterSet", log); err != nil { return err } event.Raw = log @@ -2546,21 +2962,21 @@ func (_Bridge *BridgeFilterer) WatchFraudChallengeDefeated(opts *bind.WatchOpts, }), nil } -// ParseFraudChallengeDefeated is a log parse operation binding the contract event 0x6ff720470ffad78f316655e2c7fc77a76763c13de0e19ee52149916ba7e44d3b. +// ParseEcdsaFraudRouterSet is a log parse operation binding the contract event 0x74b82ffdaa86ef071c7c5083b76052a32b9d67ead5e1013cba6979a28d1851c1. // -// Solidity: event FraudChallengeDefeated(bytes20 indexed walletPubKeyHash, bytes32 sighash) -func (_Bridge *BridgeFilterer) ParseFraudChallengeDefeated(log types.Log) (*BridgeFraudChallengeDefeated, error) { - event := new(BridgeFraudChallengeDefeated) - if err := _Bridge.contract.UnpackLog(event, "FraudChallengeDefeated", log); err != nil { +// Solidity: event EcdsaFraudRouterSet(address ecdsaFraudRouter) +func (_Bridge *BridgeFilterer) ParseEcdsaFraudRouterSet(log types.Log) (*BridgeEcdsaFraudRouterSet, error) { + event := new(BridgeEcdsaFraudRouterSet) + if err := _Bridge.contract.UnpackLog(event, "EcdsaFraudRouterSet", log); err != nil { return nil, err } event.Raw = log return event, nil } -// BridgeFraudChallengeSubmittedIterator is returned from FilterFraudChallengeSubmitted and is used to iterate over the raw logs and unpacked data for FraudChallengeSubmitted events raised by the Bridge contract. -type BridgeFraudChallengeSubmittedIterator struct { - Event *BridgeFraudChallengeSubmitted // Event containing the contract specifics and raw log +// BridgeEcdsaRetiredIterator is returned from FilterEcdsaRetired and is used to iterate over the raw logs and unpacked data for EcdsaRetired events raised by the Bridge contract. +type BridgeEcdsaRetiredIterator struct { + Event *BridgeEcdsaRetired // 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 @@ -2574,7 +2990,7 @@ type BridgeFraudChallengeSubmittedIterator struct { // 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 *BridgeFraudChallengeSubmittedIterator) Next() bool { +func (it *BridgeEcdsaRetiredIterator) Next() bool { // If the iterator failed, stop iterating if it.fail != nil { return false @@ -2583,7 +2999,7 @@ func (it *BridgeFraudChallengeSubmittedIterator) Next() bool { if it.done { select { case log := <-it.logs: - it.Event = new(BridgeFraudChallengeSubmitted) + it.Event = new(BridgeEcdsaRetired) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -2598,7 +3014,7 @@ func (it *BridgeFraudChallengeSubmittedIterator) Next() bool { // Iterator still in progress, wait for either a data or an error event select { case log := <-it.logs: - it.Event = new(BridgeFraudChallengeSubmitted) + it.Event = new(BridgeEcdsaRetired) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -2614,55 +3030,40 @@ func (it *BridgeFraudChallengeSubmittedIterator) Next() bool { } // Error returns any retrieval or parsing error occurred during filtering. -func (it *BridgeFraudChallengeSubmittedIterator) Error() error { +func (it *BridgeEcdsaRetiredIterator) Error() error { return it.fail } // Close terminates the iteration process, releasing any pending underlying // resources. -func (it *BridgeFraudChallengeSubmittedIterator) Close() error { +func (it *BridgeEcdsaRetiredIterator) Close() error { it.sub.Unsubscribe() return nil } -// BridgeFraudChallengeSubmitted represents a FraudChallengeSubmitted event raised by the Bridge contract. -type BridgeFraudChallengeSubmitted struct { - WalletPubKeyHash [20]byte - Sighash [32]byte - V uint8 - R [32]byte - S [32]byte - Raw types.Log // Blockchain specific contextual infos +// BridgeEcdsaRetired represents a EcdsaRetired event raised by the Bridge contract. +type BridgeEcdsaRetired struct { + Raw types.Log // Blockchain specific contextual infos } -// FilterFraudChallengeSubmitted is a free log retrieval operation binding the contract event 0xf4aa58d09ba5de017eac597806dfcfc2cad287816cb1eb7729a032c82680c94d. +// FilterEcdsaRetired is a free log retrieval operation binding the contract event 0xcfd6ec30c5fce5bd571f7b6c440f26edaa4ed4e92387c12806fc47ed888fd014. // -// Solidity: event FraudChallengeSubmitted(bytes20 indexed walletPubKeyHash, bytes32 sighash, uint8 v, bytes32 r, bytes32 s) -func (_Bridge *BridgeFilterer) FilterFraudChallengeSubmitted(opts *bind.FilterOpts, walletPubKeyHash [][20]byte) (*BridgeFraudChallengeSubmittedIterator, error) { - - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) - } +// Solidity: event EcdsaRetired() +func (_Bridge *BridgeFilterer) FilterEcdsaRetired(opts *bind.FilterOpts) (*BridgeEcdsaRetiredIterator, error) { - logs, sub, err := _Bridge.contract.FilterLogs(opts, "FraudChallengeSubmitted", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.FilterLogs(opts, "EcdsaRetired") if err != nil { return nil, err } - return &BridgeFraudChallengeSubmittedIterator{contract: _Bridge.contract, event: "FraudChallengeSubmitted", logs: logs, sub: sub}, nil + return &BridgeEcdsaRetiredIterator{contract: _Bridge.contract, event: "EcdsaRetired", logs: logs, sub: sub}, nil } -// WatchFraudChallengeSubmitted is a free log subscription operation binding the contract event 0xf4aa58d09ba5de017eac597806dfcfc2cad287816cb1eb7729a032c82680c94d. +// WatchEcdsaRetired is a free log subscription operation binding the contract event 0xcfd6ec30c5fce5bd571f7b6c440f26edaa4ed4e92387c12806fc47ed888fd014. // -// Solidity: event FraudChallengeSubmitted(bytes20 indexed walletPubKeyHash, bytes32 sighash, uint8 v, bytes32 r, bytes32 s) -func (_Bridge *BridgeFilterer) WatchFraudChallengeSubmitted(opts *bind.WatchOpts, sink chan<- *BridgeFraudChallengeSubmitted, walletPubKeyHash [][20]byte) (event.Subscription, error) { +// Solidity: event EcdsaRetired() +func (_Bridge *BridgeFilterer) WatchEcdsaRetired(opts *bind.WatchOpts, sink chan<- *BridgeEcdsaRetired) (event.Subscription, error) { - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) - } - - logs, sub, err := _Bridge.contract.WatchLogs(opts, "FraudChallengeSubmitted", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.WatchLogs(opts, "EcdsaRetired") if err != nil { return nil, err } @@ -2672,8 +3073,8 @@ func (_Bridge *BridgeFilterer) WatchFraudChallengeSubmitted(opts *bind.WatchOpts select { case log := <-logs: // New log arrived, parse the event and forward to the user - event := new(BridgeFraudChallengeSubmitted) - if err := _Bridge.contract.UnpackLog(event, "FraudChallengeSubmitted", log); err != nil { + event := new(BridgeEcdsaRetired) + if err := _Bridge.contract.UnpackLog(event, "EcdsaRetired", log); err != nil { return err } event.Raw = log @@ -2694,12 +3095,12 @@ func (_Bridge *BridgeFilterer) WatchFraudChallengeSubmitted(opts *bind.WatchOpts }), nil } -// ParseFraudChallengeSubmitted is a log parse operation binding the contract event 0xf4aa58d09ba5de017eac597806dfcfc2cad287816cb1eb7729a032c82680c94d. +// ParseEcdsaRetired is a log parse operation binding the contract event 0xcfd6ec30c5fce5bd571f7b6c440f26edaa4ed4e92387c12806fc47ed888fd014. // -// Solidity: event FraudChallengeSubmitted(bytes20 indexed walletPubKeyHash, bytes32 sighash, uint8 v, bytes32 r, bytes32 s) -func (_Bridge *BridgeFilterer) ParseFraudChallengeSubmitted(log types.Log) (*BridgeFraudChallengeSubmitted, error) { - event := new(BridgeFraudChallengeSubmitted) - if err := _Bridge.contract.UnpackLog(event, "FraudChallengeSubmitted", log); err != nil { +// Solidity: event EcdsaRetired() +func (_Bridge *BridgeFilterer) ParseEcdsaRetired(log types.Log) (*BridgeEcdsaRetired, error) { + event := new(BridgeEcdsaRetired) + if err := _Bridge.contract.UnpackLog(event, "EcdsaRetired", log); err != nil { return nil, err } event.Raw = log @@ -2843,6 +3244,140 @@ func (_Bridge *BridgeFilterer) ParseFraudParametersUpdated(log types.Log) (*Brid return event, nil } +// BridgeFrostWalletRegistrySetIterator is returned from FilterFrostWalletRegistrySet and is used to iterate over the raw logs and unpacked data for FrostWalletRegistrySet events raised by the Bridge contract. +type BridgeFrostWalletRegistrySetIterator struct { + Event *BridgeFrostWalletRegistrySet // 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 *BridgeFrostWalletRegistrySetIterator) 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(BridgeFrostWalletRegistrySet) + 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(BridgeFrostWalletRegistrySet) + 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 *BridgeFrostWalletRegistrySetIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeFrostWalletRegistrySetIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeFrostWalletRegistrySet represents a FrostWalletRegistrySet event raised by the Bridge contract. +type BridgeFrostWalletRegistrySet struct { + FrostWalletRegistry common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterFrostWalletRegistrySet is a free log retrieval operation binding the contract event 0xdbe373e942a6a777b9b8e4970445ff3dee716310d6d5d2265c7b01947776b6df. +// +// Solidity: event FrostWalletRegistrySet(address frostWalletRegistry) +func (_Bridge *BridgeFilterer) FilterFrostWalletRegistrySet(opts *bind.FilterOpts) (*BridgeFrostWalletRegistrySetIterator, error) { + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "FrostWalletRegistrySet") + if err != nil { + return nil, err + } + return &BridgeFrostWalletRegistrySetIterator{contract: _Bridge.contract, event: "FrostWalletRegistrySet", logs: logs, sub: sub}, nil +} + +// WatchFrostWalletRegistrySet is a free log subscription operation binding the contract event 0xdbe373e942a6a777b9b8e4970445ff3dee716310d6d5d2265c7b01947776b6df. +// +// Solidity: event FrostWalletRegistrySet(address frostWalletRegistry) +func (_Bridge *BridgeFilterer) WatchFrostWalletRegistrySet(opts *bind.WatchOpts, sink chan<- *BridgeFrostWalletRegistrySet) (event.Subscription, error) { + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "FrostWalletRegistrySet") + 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(BridgeFrostWalletRegistrySet) + if err := _Bridge.contract.UnpackLog(event, "FrostWalletRegistrySet", 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 +} + +// ParseFrostWalletRegistrySet is a log parse operation binding the contract event 0xdbe373e942a6a777b9b8e4970445ff3dee716310d6d5d2265c7b01947776b6df. +// +// Solidity: event FrostWalletRegistrySet(address frostWalletRegistry) +func (_Bridge *BridgeFilterer) ParseFrostWalletRegistrySet(log types.Log) (*BridgeFrostWalletRegistrySet, error) { + event := new(BridgeFrostWalletRegistrySet) + if err := _Bridge.contract.UnpackLog(event, "FrostWalletRegistrySet", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // BridgeGovernanceTransferredIterator is returned from FilterGovernanceTransferred and is used to iterate over the raw logs and unpacked data for GovernanceTransferred events raised by the Bridge contract. type BridgeGovernanceTransferredIterator struct { Event *BridgeGovernanceTransferred // Event containing the contract specifics and raw log @@ -3112,9 +3647,9 @@ func (_Bridge *BridgeFilterer) ParseInitialized(log types.Log) (*BridgeInitializ return event, nil } -// BridgeMovedFundsSweepTimedOutIterator is returned from FilterMovedFundsSweepTimedOut and is used to iterate over the raw logs and unpacked data for MovedFundsSweepTimedOut events raised by the Bridge contract. -type BridgeMovedFundsSweepTimedOutIterator struct { - Event *BridgeMovedFundsSweepTimedOut // Event containing the contract specifics and raw log +// BridgeLegacyFraudChallengeMigratedIterator is returned from FilterLegacyFraudChallengeMigrated and is used to iterate over the raw logs and unpacked data for LegacyFraudChallengeMigrated events raised by the Bridge contract. +type BridgeLegacyFraudChallengeMigratedIterator struct { + Event *BridgeLegacyFraudChallengeMigrated // 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 @@ -3128,7 +3663,7 @@ type BridgeMovedFundsSweepTimedOutIterator struct { // 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 *BridgeMovedFundsSweepTimedOutIterator) Next() bool { +func (it *BridgeLegacyFraudChallengeMigratedIterator) Next() bool { // If the iterator failed, stop iterating if it.fail != nil { return false @@ -3137,7 +3672,7 @@ func (it *BridgeMovedFundsSweepTimedOutIterator) Next() bool { if it.done { select { case log := <-it.logs: - it.Event = new(BridgeMovedFundsSweepTimedOut) + it.Event = new(BridgeLegacyFraudChallengeMigrated) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -3152,7 +3687,7 @@ func (it *BridgeMovedFundsSweepTimedOutIterator) Next() bool { // Iterator still in progress, wait for either a data or an error event select { case log := <-it.logs: - it.Event = new(BridgeMovedFundsSweepTimedOut) + it.Event = new(BridgeLegacyFraudChallengeMigrated) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -3168,50 +3703,347 @@ func (it *BridgeMovedFundsSweepTimedOutIterator) Next() bool { } // Error returns any retrieval or parsing error occurred during filtering. -func (it *BridgeMovedFundsSweepTimedOutIterator) Error() error { +func (it *BridgeLegacyFraudChallengeMigratedIterator) Error() error { return it.fail } // Close terminates the iteration process, releasing any pending underlying // resources. -func (it *BridgeMovedFundsSweepTimedOutIterator) Close() error { +func (it *BridgeLegacyFraudChallengeMigratedIterator) Close() error { it.sub.Unsubscribe() return nil } -// BridgeMovedFundsSweepTimedOut represents a MovedFundsSweepTimedOut event raised by the Bridge contract. -type BridgeMovedFundsSweepTimedOut struct { - WalletPubKeyHash [20]byte - MovingFundsTxHash [32]byte - MovingFundsTxOutputIndex uint32 - Raw types.Log // Blockchain specific contextual infos +// BridgeLegacyFraudChallengeMigrated represents a LegacyFraudChallengeMigrated event raised by the Bridge contract. +type BridgeLegacyFraudChallengeMigrated struct { + RouterKind uint8 + ChallengeKey *big.Int + Challenger common.Address + DepositAmount *big.Int + Raw types.Log // Blockchain specific contextual infos } -// FilterMovedFundsSweepTimedOut is a free log retrieval operation binding the contract event 0x4c25d874672bd8dcc921a53387892a0d5c26d5dae3d368ffe83f65cc99700612. +// FilterLegacyFraudChallengeMigrated is a free log retrieval operation binding the contract event 0xef4dd86f5d8e036d15cf4958485bdef0a43da00304fa8ad123bda135dfca8f8f. // -// Solidity: event MovedFundsSweepTimedOut(bytes20 indexed walletPubKeyHash, bytes32 movingFundsTxHash, uint32 movingFundsTxOutputIndex) -func (_Bridge *BridgeFilterer) FilterMovedFundsSweepTimedOut(opts *bind.FilterOpts, walletPubKeyHash [][20]byte) (*BridgeMovedFundsSweepTimedOutIterator, error) { +// Solidity: event LegacyFraudChallengeMigrated(uint8 indexed routerKind, uint256 indexed challengeKey, address indexed challenger, uint256 depositAmount) +func (_Bridge *BridgeFilterer) FilterLegacyFraudChallengeMigrated(opts *bind.FilterOpts, routerKind []uint8, challengeKey []*big.Int, challenger []common.Address) (*BridgeLegacyFraudChallengeMigratedIterator, error) { - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + var routerKindRule []interface{} + for _, routerKindItem := range routerKind { + routerKindRule = append(routerKindRule, routerKindItem) + } + var challengeKeyRule []interface{} + for _, challengeKeyItem := range challengeKey { + challengeKeyRule = append(challengeKeyRule, challengeKeyItem) + } + var challengerRule []interface{} + for _, challengerItem := range challenger { + challengerRule = append(challengerRule, challengerItem) } - logs, sub, err := _Bridge.contract.FilterLogs(opts, "MovedFundsSweepTimedOut", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.FilterLogs(opts, "LegacyFraudChallengeMigrated", routerKindRule, challengeKeyRule, challengerRule) if err != nil { return nil, err } - return &BridgeMovedFundsSweepTimedOutIterator{contract: _Bridge.contract, event: "MovedFundsSweepTimedOut", logs: logs, sub: sub}, nil + return &BridgeLegacyFraudChallengeMigratedIterator{contract: _Bridge.contract, event: "LegacyFraudChallengeMigrated", logs: logs, sub: sub}, nil } -// WatchMovedFundsSweepTimedOut is a free log subscription operation binding the contract event 0x4c25d874672bd8dcc921a53387892a0d5c26d5dae3d368ffe83f65cc99700612. +// WatchLegacyFraudChallengeMigrated is a free log subscription operation binding the contract event 0xef4dd86f5d8e036d15cf4958485bdef0a43da00304fa8ad123bda135dfca8f8f. // -// Solidity: event MovedFundsSweepTimedOut(bytes20 indexed walletPubKeyHash, bytes32 movingFundsTxHash, uint32 movingFundsTxOutputIndex) -func (_Bridge *BridgeFilterer) WatchMovedFundsSweepTimedOut(opts *bind.WatchOpts, sink chan<- *BridgeMovedFundsSweepTimedOut, walletPubKeyHash [][20]byte) (event.Subscription, error) { +// Solidity: event LegacyFraudChallengeMigrated(uint8 indexed routerKind, uint256 indexed challengeKey, address indexed challenger, uint256 depositAmount) +func (_Bridge *BridgeFilterer) WatchLegacyFraudChallengeMigrated(opts *bind.WatchOpts, sink chan<- *BridgeLegacyFraudChallengeMigrated, routerKind []uint8, challengeKey []*big.Int, challenger []common.Address) (event.Subscription, error) { - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + var routerKindRule []interface{} + for _, routerKindItem := range routerKind { + routerKindRule = append(routerKindRule, routerKindItem) + } + var challengeKeyRule []interface{} + for _, challengeKeyItem := range challengeKey { + challengeKeyRule = append(challengeKeyRule, challengeKeyItem) + } + var challengerRule []interface{} + for _, challengerItem := range challenger { + challengerRule = append(challengerRule, challengerItem) + } + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "LegacyFraudChallengeMigrated", routerKindRule, challengeKeyRule, challengerRule) + 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(BridgeLegacyFraudChallengeMigrated) + if err := _Bridge.contract.UnpackLog(event, "LegacyFraudChallengeMigrated", 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 +} + +// ParseLegacyFraudChallengeMigrated is a log parse operation binding the contract event 0xef4dd86f5d8e036d15cf4958485bdef0a43da00304fa8ad123bda135dfca8f8f. +// +// Solidity: event LegacyFraudChallengeMigrated(uint8 indexed routerKind, uint256 indexed challengeKey, address indexed challenger, uint256 depositAmount) +func (_Bridge *BridgeFilterer) ParseLegacyFraudChallengeMigrated(log types.Log) (*BridgeLegacyFraudChallengeMigrated, error) { + event := new(BridgeLegacyFraudChallengeMigrated) + if err := _Bridge.contract.UnpackLog(event, "LegacyFraudChallengeMigrated", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BridgeLifecycleRouterSetIterator is returned from FilterLifecycleRouterSet and is used to iterate over the raw logs and unpacked data for LifecycleRouterSet events raised by the Bridge contract. +type BridgeLifecycleRouterSetIterator struct { + Event *BridgeLifecycleRouterSet // 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 *BridgeLifecycleRouterSetIterator) 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(BridgeLifecycleRouterSet) + 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(BridgeLifecycleRouterSet) + 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 *BridgeLifecycleRouterSetIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeLifecycleRouterSetIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeLifecycleRouterSet represents a LifecycleRouterSet event raised by the Bridge contract. +type BridgeLifecycleRouterSet struct { + LifecycleRouter common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterLifecycleRouterSet is a free log retrieval operation binding the contract event 0xd34c360c4ba3b0ef69ec75dd2fd413d2432504b21290e9dfdd9d0bffab5376d7. +// +// Solidity: event LifecycleRouterSet(address lifecycleRouter) +func (_Bridge *BridgeFilterer) FilterLifecycleRouterSet(opts *bind.FilterOpts) (*BridgeLifecycleRouterSetIterator, error) { + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "LifecycleRouterSet") + if err != nil { + return nil, err + } + return &BridgeLifecycleRouterSetIterator{contract: _Bridge.contract, event: "LifecycleRouterSet", logs: logs, sub: sub}, nil +} + +// WatchLifecycleRouterSet is a free log subscription operation binding the contract event 0xd34c360c4ba3b0ef69ec75dd2fd413d2432504b21290e9dfdd9d0bffab5376d7. +// +// Solidity: event LifecycleRouterSet(address lifecycleRouter) +func (_Bridge *BridgeFilterer) WatchLifecycleRouterSet(opts *bind.WatchOpts, sink chan<- *BridgeLifecycleRouterSet) (event.Subscription, error) { + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "LifecycleRouterSet") + 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(BridgeLifecycleRouterSet) + if err := _Bridge.contract.UnpackLog(event, "LifecycleRouterSet", 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 +} + +// ParseLifecycleRouterSet is a log parse operation binding the contract event 0xd34c360c4ba3b0ef69ec75dd2fd413d2432504b21290e9dfdd9d0bffab5376d7. +// +// Solidity: event LifecycleRouterSet(address lifecycleRouter) +func (_Bridge *BridgeFilterer) ParseLifecycleRouterSet(log types.Log) (*BridgeLifecycleRouterSet, error) { + event := new(BridgeLifecycleRouterSet) + if err := _Bridge.contract.UnpackLog(event, "LifecycleRouterSet", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BridgeMovedFundsSweepTimedOutIterator is returned from FilterMovedFundsSweepTimedOut and is used to iterate over the raw logs and unpacked data for MovedFundsSweepTimedOut events raised by the Bridge contract. +type BridgeMovedFundsSweepTimedOutIterator struct { + Event *BridgeMovedFundsSweepTimedOut // 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 *BridgeMovedFundsSweepTimedOutIterator) 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(BridgeMovedFundsSweepTimedOut) + 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(BridgeMovedFundsSweepTimedOut) + 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 *BridgeMovedFundsSweepTimedOutIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeMovedFundsSweepTimedOutIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeMovedFundsSweepTimedOut represents a MovedFundsSweepTimedOut event raised by the Bridge contract. +type BridgeMovedFundsSweepTimedOut struct { + WalletPubKeyHash [20]byte + MovingFundsTxHash [32]byte + MovingFundsTxOutputIndex uint32 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterMovedFundsSweepTimedOut is a free log retrieval operation binding the contract event 0x4c25d874672bd8dcc921a53387892a0d5c26d5dae3d368ffe83f65cc99700612. +// +// Solidity: event MovedFundsSweepTimedOut(bytes20 indexed walletPubKeyHash, bytes32 movingFundsTxHash, uint32 movingFundsTxOutputIndex) +func (_Bridge *BridgeFilterer) FilterMovedFundsSweepTimedOut(opts *bind.FilterOpts, walletPubKeyHash [][20]byte) (*BridgeMovedFundsSweepTimedOutIterator, error) { + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "MovedFundsSweepTimedOut", walletPubKeyHashRule) + if err != nil { + return nil, err + } + return &BridgeMovedFundsSweepTimedOutIterator{contract: _Bridge.contract, event: "MovedFundsSweepTimedOut", logs: logs, sub: sub}, nil +} + +// WatchMovedFundsSweepTimedOut is a free log subscription operation binding the contract event 0x4c25d874672bd8dcc921a53387892a0d5c26d5dae3d368ffe83f65cc99700612. +// +// Solidity: event MovedFundsSweepTimedOut(bytes20 indexed walletPubKeyHash, bytes32 movingFundsTxHash, uint32 movingFundsTxOutputIndex) +func (_Bridge *BridgeFilterer) WatchMovedFundsSweepTimedOut(opts *bind.WatchOpts, sink chan<- *BridgeMovedFundsSweepTimedOut, walletPubKeyHash [][20]byte) (event.Subscription, error) { + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) } logs, sub, err := _Bridge.contract.WatchLogs(opts, "MovedFundsSweepTimedOut", walletPubKeyHashRule) @@ -4082,7 +4914,761 @@ func (_Bridge *BridgeFilterer) WatchMovingFundsTimedOut(opts *bind.WatchOpts, si walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) } - logs, sub, err := _Bridge.contract.WatchLogs(opts, "MovingFundsTimedOut", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.WatchLogs(opts, "MovingFundsTimedOut", 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(BridgeMovingFundsTimedOut) + if err := _Bridge.contract.UnpackLog(event, "MovingFundsTimedOut", 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 +} + +// ParseMovingFundsTimedOut is a log parse operation binding the contract event 0x5862a5a7095622ec6e3a04e5bb6547f1e4034af0d7d4d7e9787678072fc66fb2. +// +// Solidity: event MovingFundsTimedOut(bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) ParseMovingFundsTimedOut(log types.Log) (*BridgeMovingFundsTimedOut, error) { + event := new(BridgeMovingFundsTimedOut) + if err := _Bridge.contract.UnpackLog(event, "MovingFundsTimedOut", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BridgeMovingFundsTimeoutResetIterator is returned from FilterMovingFundsTimeoutReset and is used to iterate over the raw logs and unpacked data for MovingFundsTimeoutReset events raised by the Bridge contract. +type BridgeMovingFundsTimeoutResetIterator struct { + Event *BridgeMovingFundsTimeoutReset // 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 *BridgeMovingFundsTimeoutResetIterator) 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(BridgeMovingFundsTimeoutReset) + 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(BridgeMovingFundsTimeoutReset) + 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 *BridgeMovingFundsTimeoutResetIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeMovingFundsTimeoutResetIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeMovingFundsTimeoutReset represents a MovingFundsTimeoutReset event raised by the Bridge contract. +type BridgeMovingFundsTimeoutReset struct { + WalletPubKeyHash [20]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterMovingFundsTimeoutReset is a free log retrieval operation binding the contract event 0xa59c6e2153c28ecd6a3507cfa44c3ab392779ff484a1aee02e40345c63a59bc0. +// +// Solidity: event MovingFundsTimeoutReset(bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) FilterMovingFundsTimeoutReset(opts *bind.FilterOpts, walletPubKeyHash [][20]byte) (*BridgeMovingFundsTimeoutResetIterator, error) { + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "MovingFundsTimeoutReset", walletPubKeyHashRule) + if err != nil { + return nil, err + } + return &BridgeMovingFundsTimeoutResetIterator{contract: _Bridge.contract, event: "MovingFundsTimeoutReset", logs: logs, sub: sub}, nil +} + +// WatchMovingFundsTimeoutReset is a free log subscription operation binding the contract event 0xa59c6e2153c28ecd6a3507cfa44c3ab392779ff484a1aee02e40345c63a59bc0. +// +// Solidity: event MovingFundsTimeoutReset(bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) WatchMovingFundsTimeoutReset(opts *bind.WatchOpts, sink chan<- *BridgeMovingFundsTimeoutReset, walletPubKeyHash [][20]byte) (event.Subscription, error) { + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "MovingFundsTimeoutReset", 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(BridgeMovingFundsTimeoutReset) + if err := _Bridge.contract.UnpackLog(event, "MovingFundsTimeoutReset", 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 +} + +// ParseMovingFundsTimeoutReset is a log parse operation binding the contract event 0xa59c6e2153c28ecd6a3507cfa44c3ab392779ff484a1aee02e40345c63a59bc0. +// +// Solidity: event MovingFundsTimeoutReset(bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) ParseMovingFundsTimeoutReset(log types.Log) (*BridgeMovingFundsTimeoutReset, error) { + event := new(BridgeMovingFundsTimeoutReset) + if err := _Bridge.contract.UnpackLog(event, "MovingFundsTimeoutReset", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BridgeNewFrostWalletRegisteredIterator is returned from FilterNewFrostWalletRegistered and is used to iterate over the raw logs and unpacked data for NewFrostWalletRegistered events raised by the Bridge contract. +type BridgeNewFrostWalletRegisteredIterator struct { + Event *BridgeNewFrostWalletRegistered // 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 *BridgeNewFrostWalletRegisteredIterator) 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(BridgeNewFrostWalletRegistered) + 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(BridgeNewFrostWalletRegistered) + 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 *BridgeNewFrostWalletRegisteredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeNewFrostWalletRegisteredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeNewFrostWalletRegistered represents a NewFrostWalletRegistered event raised by the Bridge contract. +type BridgeNewFrostWalletRegistered struct { + WalletID [32]byte + WalletPubKeyHash [20]byte + XOnlyOutputKey [32]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNewFrostWalletRegistered is a free log retrieval operation binding the contract event 0xd9aa9c3636339f9edab116054e0fff7f31ff75da8fb201345c31980bb7644334. +// +// Solidity: event NewFrostWalletRegistered(bytes32 indexed walletID, bytes20 indexed walletPubKeyHash, bytes32 indexed xOnlyOutputKey) +func (_Bridge *BridgeFilterer) FilterNewFrostWalletRegistered(opts *bind.FilterOpts, walletID [][32]byte, walletPubKeyHash [][20]byte, xOnlyOutputKey [][32]byte) (*BridgeNewFrostWalletRegisteredIterator, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + var xOnlyOutputKeyRule []interface{} + for _, xOnlyOutputKeyItem := range xOnlyOutputKey { + xOnlyOutputKeyRule = append(xOnlyOutputKeyRule, xOnlyOutputKeyItem) + } + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "NewFrostWalletRegistered", walletIDRule, walletPubKeyHashRule, xOnlyOutputKeyRule) + if err != nil { + return nil, err + } + return &BridgeNewFrostWalletRegisteredIterator{contract: _Bridge.contract, event: "NewFrostWalletRegistered", logs: logs, sub: sub}, nil +} + +// WatchNewFrostWalletRegistered is a free log subscription operation binding the contract event 0xd9aa9c3636339f9edab116054e0fff7f31ff75da8fb201345c31980bb7644334. +// +// Solidity: event NewFrostWalletRegistered(bytes32 indexed walletID, bytes20 indexed walletPubKeyHash, bytes32 indexed xOnlyOutputKey) +func (_Bridge *BridgeFilterer) WatchNewFrostWalletRegistered(opts *bind.WatchOpts, sink chan<- *BridgeNewFrostWalletRegistered, walletID [][32]byte, walletPubKeyHash [][20]byte, xOnlyOutputKey [][32]byte) (event.Subscription, error) { + + var walletIDRule []interface{} + for _, walletIDItem := range walletID { + walletIDRule = append(walletIDRule, walletIDItem) + } + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + var xOnlyOutputKeyRule []interface{} + for _, xOnlyOutputKeyItem := range xOnlyOutputKey { + xOnlyOutputKeyRule = append(xOnlyOutputKeyRule, xOnlyOutputKeyItem) + } + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "NewFrostWalletRegistered", walletIDRule, walletPubKeyHashRule, xOnlyOutputKeyRule) + 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(BridgeNewFrostWalletRegistered) + if err := _Bridge.contract.UnpackLog(event, "NewFrostWalletRegistered", 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 +} + +// ParseNewFrostWalletRegistered is a log parse operation binding the contract event 0xd9aa9c3636339f9edab116054e0fff7f31ff75da8fb201345c31980bb7644334. +// +// Solidity: event NewFrostWalletRegistered(bytes32 indexed walletID, bytes20 indexed walletPubKeyHash, bytes32 indexed xOnlyOutputKey) +func (_Bridge *BridgeFilterer) ParseNewFrostWalletRegistered(log types.Log) (*BridgeNewFrostWalletRegistered, error) { + event := new(BridgeNewFrostWalletRegistered) + if err := _Bridge.contract.UnpackLog(event, "NewFrostWalletRegistered", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +// BridgeNewWalletRegisteredIterator is returned from FilterNewWalletRegistered and is used to iterate over the raw logs and unpacked data for NewWalletRegistered events raised by the Bridge contract. +type BridgeNewWalletRegisteredIterator struct { + Event *BridgeNewWalletRegistered // 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 *BridgeNewWalletRegisteredIterator) 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(BridgeNewWalletRegistered) + 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(BridgeNewWalletRegistered) + 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 *BridgeNewWalletRegisteredIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeNewWalletRegisteredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeNewWalletRegistered represents a NewWalletRegistered event raised by the Bridge contract. +type BridgeNewWalletRegistered struct { + EcdsaWalletID [32]byte + WalletPubKeyHash [20]byte + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNewWalletRegistered is a free log retrieval operation binding the contract event 0x2dbb47dce81d6b11cca1f1e3b10143d6f7e1e7e92d2dd9aacbb1f875379d308e. +// +// Solidity: event NewWalletRegistered(bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) FilterNewWalletRegistered(opts *bind.FilterOpts, ecdsaWalletID [][32]byte, walletPubKeyHash [][20]byte) (*BridgeNewWalletRegisteredIterator, error) { + + 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, "NewWalletRegistered", ecdsaWalletIDRule, walletPubKeyHashRule) + if err != nil { + return nil, err + } + return &BridgeNewWalletRegisteredIterator{contract: _Bridge.contract, event: "NewWalletRegistered", logs: logs, sub: sub}, nil +} + +// WatchNewWalletRegistered is a free log subscription operation binding the contract event 0x2dbb47dce81d6b11cca1f1e3b10143d6f7e1e7e92d2dd9aacbb1f875379d308e. +// +// Solidity: event NewWalletRegistered(bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) WatchNewWalletRegistered(opts *bind.WatchOpts, sink chan<- *BridgeNewWalletRegistered, ecdsaWalletID [][32]byte, walletPubKeyHash [][20]byte) (event.Subscription, error) { + + 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, "NewWalletRegistered", 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(BridgeNewWalletRegistered) + if err := _Bridge.contract.UnpackLog(event, "NewWalletRegistered", 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 +} + +// ParseNewWalletRegistered is a log parse operation binding the contract event 0x2dbb47dce81d6b11cca1f1e3b10143d6f7e1e7e92d2dd9aacbb1f875379d308e. +// +// Solidity: event NewWalletRegistered(bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) +func (_Bridge *BridgeFilterer) ParseNewWalletRegistered(log types.Log) (*BridgeNewWalletRegistered, error) { + event := new(BridgeNewWalletRegistered) + if err := _Bridge.contract.UnpackLog(event, "NewWalletRegistered", log); err != nil { + return nil, err + } + event.Raw = log + 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 + + 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 *BridgeNewWalletRequestedIterator) 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(BridgeNewWalletRequested) + 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(BridgeNewWalletRequested) + 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 *BridgeNewWalletRequestedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeNewWalletRequestedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeNewWalletRequested represents a NewWalletRequested event raised by the Bridge contract. +type BridgeNewWalletRequested struct { + Raw types.Log // Blockchain specific contextual infos +} + +// FilterNewWalletRequested is a free log retrieval operation binding the contract event 0x31fecb80caf1e1128496dd5a6f1083ba29fd5fe64c3fe04e2d1b6f9cfc27d5a3. +// +// Solidity: event NewWalletRequested() +func (_Bridge *BridgeFilterer) FilterNewWalletRequested(opts *bind.FilterOpts) (*BridgeNewWalletRequestedIterator, error) { + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "NewWalletRequested") + if err != nil { + return nil, err + } + return &BridgeNewWalletRequestedIterator{contract: _Bridge.contract, event: "NewWalletRequested", logs: logs, sub: sub}, nil +} + +// WatchNewWalletRequested is a free log subscription operation binding the contract event 0x31fecb80caf1e1128496dd5a6f1083ba29fd5fe64c3fe04e2d1b6f9cfc27d5a3. +// +// Solidity: event NewWalletRequested() +func (_Bridge *BridgeFilterer) WatchNewWalletRequested(opts *bind.WatchOpts, sink chan<- *BridgeNewWalletRequested) (event.Subscription, error) { + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "NewWalletRequested") if err != nil { return nil, err } @@ -4092,8 +5678,8 @@ func (_Bridge *BridgeFilterer) WatchMovingFundsTimedOut(opts *bind.WatchOpts, si select { case log := <-logs: // New log arrived, parse the event and forward to the user - event := new(BridgeMovingFundsTimedOut) - if err := _Bridge.contract.UnpackLog(event, "MovingFundsTimedOut", log); err != nil { + event := new(BridgeNewWalletRequested) + if err := _Bridge.contract.UnpackLog(event, "NewWalletRequested", log); err != nil { return err } event.Raw = log @@ -4114,21 +5700,21 @@ func (_Bridge *BridgeFilterer) WatchMovingFundsTimedOut(opts *bind.WatchOpts, si }), nil } -// ParseMovingFundsTimedOut is a log parse operation binding the contract event 0x5862a5a7095622ec6e3a04e5bb6547f1e4034af0d7d4d7e9787678072fc66fb2. +// ParseNewWalletRequested is a log parse operation binding the contract event 0x31fecb80caf1e1128496dd5a6f1083ba29fd5fe64c3fe04e2d1b6f9cfc27d5a3. // -// Solidity: event MovingFundsTimedOut(bytes20 indexed walletPubKeyHash) -func (_Bridge *BridgeFilterer) ParseMovingFundsTimedOut(log types.Log) (*BridgeMovingFundsTimedOut, error) { - event := new(BridgeMovingFundsTimedOut) - if err := _Bridge.contract.UnpackLog(event, "MovingFundsTimedOut", log); err != nil { +// Solidity: event NewWalletRequested() +func (_Bridge *BridgeFilterer) ParseNewWalletRequested(log types.Log) (*BridgeNewWalletRequested, error) { + event := new(BridgeNewWalletRequested) + if err := _Bridge.contract.UnpackLog(event, "NewWalletRequested", log); err != nil { return nil, err } event.Raw = log return event, nil } -// BridgeMovingFundsTimeoutResetIterator is returned from FilterMovingFundsTimeoutReset and is used to iterate over the raw logs and unpacked data for MovingFundsTimeoutReset events raised by the Bridge contract. -type BridgeMovingFundsTimeoutResetIterator struct { - Event *BridgeMovingFundsTimeoutReset // Event containing the contract specifics and raw log +// BridgeNewWalletSchemeSetIterator is returned from FilterNewWalletSchemeSet and is used to iterate over the raw logs and unpacked data for NewWalletSchemeSet events raised by the Bridge contract. +type BridgeNewWalletSchemeSetIterator struct { + Event *BridgeNewWalletSchemeSet // 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 @@ -4142,7 +5728,7 @@ type BridgeMovingFundsTimeoutResetIterator struct { // 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 *BridgeMovingFundsTimeoutResetIterator) Next() bool { +func (it *BridgeNewWalletSchemeSetIterator) Next() bool { // If the iterator failed, stop iterating if it.fail != nil { return false @@ -4151,7 +5737,7 @@ func (it *BridgeMovingFundsTimeoutResetIterator) Next() bool { if it.done { select { case log := <-it.logs: - it.Event = new(BridgeMovingFundsTimeoutReset) + it.Event = new(BridgeNewWalletSchemeSet) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -4166,7 +5752,7 @@ func (it *BridgeMovingFundsTimeoutResetIterator) Next() bool { // Iterator still in progress, wait for either a data or an error event select { case log := <-it.logs: - it.Event = new(BridgeMovingFundsTimeoutReset) + it.Event = new(BridgeNewWalletSchemeSet) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -4182,51 +5768,51 @@ func (it *BridgeMovingFundsTimeoutResetIterator) Next() bool { } // Error returns any retrieval or parsing error occurred during filtering. -func (it *BridgeMovingFundsTimeoutResetIterator) Error() error { +func (it *BridgeNewWalletSchemeSetIterator) Error() error { return it.fail } // Close terminates the iteration process, releasing any pending underlying // resources. -func (it *BridgeMovingFundsTimeoutResetIterator) Close() error { +func (it *BridgeNewWalletSchemeSetIterator) Close() error { it.sub.Unsubscribe() return nil } -// BridgeMovingFundsTimeoutReset represents a MovingFundsTimeoutReset event raised by the Bridge contract. -type BridgeMovingFundsTimeoutReset struct { - WalletPubKeyHash [20]byte - Raw types.Log // Blockchain specific contextual infos +// BridgeNewWalletSchemeSet represents a NewWalletSchemeSet event raised by the Bridge contract. +type BridgeNewWalletSchemeSet struct { + Scheme uint8 + Raw types.Log // Blockchain specific contextual infos } -// FilterMovingFundsTimeoutReset is a free log retrieval operation binding the contract event 0xa59c6e2153c28ecd6a3507cfa44c3ab392779ff484a1aee02e40345c63a59bc0. +// FilterNewWalletSchemeSet is a free log retrieval operation binding the contract event 0xf02f991b885946929457e15df17c468398baff309f97deb150209e448b9157ca. // -// Solidity: event MovingFundsTimeoutReset(bytes20 indexed walletPubKeyHash) -func (_Bridge *BridgeFilterer) FilterMovingFundsTimeoutReset(opts *bind.FilterOpts, walletPubKeyHash [][20]byte) (*BridgeMovingFundsTimeoutResetIterator, error) { +// Solidity: event NewWalletSchemeSet(uint8 indexed scheme) +func (_Bridge *BridgeFilterer) FilterNewWalletSchemeSet(opts *bind.FilterOpts, scheme []uint8) (*BridgeNewWalletSchemeSetIterator, error) { - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + var schemeRule []interface{} + for _, schemeItem := range scheme { + schemeRule = append(schemeRule, schemeItem) } - logs, sub, err := _Bridge.contract.FilterLogs(opts, "MovingFundsTimeoutReset", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.FilterLogs(opts, "NewWalletSchemeSet", schemeRule) if err != nil { return nil, err } - return &BridgeMovingFundsTimeoutResetIterator{contract: _Bridge.contract, event: "MovingFundsTimeoutReset", logs: logs, sub: sub}, nil + return &BridgeNewWalletSchemeSetIterator{contract: _Bridge.contract, event: "NewWalletSchemeSet", logs: logs, sub: sub}, nil } -// WatchMovingFundsTimeoutReset is a free log subscription operation binding the contract event 0xa59c6e2153c28ecd6a3507cfa44c3ab392779ff484a1aee02e40345c63a59bc0. +// WatchNewWalletSchemeSet is a free log subscription operation binding the contract event 0xf02f991b885946929457e15df17c468398baff309f97deb150209e448b9157ca. // -// Solidity: event MovingFundsTimeoutReset(bytes20 indexed walletPubKeyHash) -func (_Bridge *BridgeFilterer) WatchMovingFundsTimeoutReset(opts *bind.WatchOpts, sink chan<- *BridgeMovingFundsTimeoutReset, walletPubKeyHash [][20]byte) (event.Subscription, error) { +// Solidity: event NewWalletSchemeSet(uint8 indexed scheme) +func (_Bridge *BridgeFilterer) WatchNewWalletSchemeSet(opts *bind.WatchOpts, sink chan<- *BridgeNewWalletSchemeSet, scheme []uint8) (event.Subscription, error) { - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + var schemeRule []interface{} + for _, schemeItem := range scheme { + schemeRule = append(schemeRule, schemeItem) } - logs, sub, err := _Bridge.contract.WatchLogs(opts, "MovingFundsTimeoutReset", walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.WatchLogs(opts, "NewWalletSchemeSet", schemeRule) if err != nil { return nil, err } @@ -4236,8 +5822,8 @@ func (_Bridge *BridgeFilterer) WatchMovingFundsTimeoutReset(opts *bind.WatchOpts select { case log := <-logs: // New log arrived, parse the event and forward to the user - event := new(BridgeMovingFundsTimeoutReset) - if err := _Bridge.contract.UnpackLog(event, "MovingFundsTimeoutReset", log); err != nil { + event := new(BridgeNewWalletSchemeSet) + if err := _Bridge.contract.UnpackLog(event, "NewWalletSchemeSet", log); err != nil { return err } event.Raw = log @@ -4258,21 +5844,21 @@ func (_Bridge *BridgeFilterer) WatchMovingFundsTimeoutReset(opts *bind.WatchOpts }), nil } -// ParseMovingFundsTimeoutReset is a log parse operation binding the contract event 0xa59c6e2153c28ecd6a3507cfa44c3ab392779ff484a1aee02e40345c63a59bc0. +// ParseNewWalletSchemeSet is a log parse operation binding the contract event 0xf02f991b885946929457e15df17c468398baff309f97deb150209e448b9157ca. // -// Solidity: event MovingFundsTimeoutReset(bytes20 indexed walletPubKeyHash) -func (_Bridge *BridgeFilterer) ParseMovingFundsTimeoutReset(log types.Log) (*BridgeMovingFundsTimeoutReset, error) { - event := new(BridgeMovingFundsTimeoutReset) - if err := _Bridge.contract.UnpackLog(event, "MovingFundsTimeoutReset", log); err != nil { +// Solidity: event NewWalletSchemeSet(uint8 indexed scheme) +func (_Bridge *BridgeFilterer) ParseNewWalletSchemeSet(log types.Log) (*BridgeNewWalletSchemeSet, error) { + event := new(BridgeNewWalletSchemeSet) + if err := _Bridge.contract.UnpackLog(event, "NewWalletSchemeSet", log); err != nil { return nil, err } event.Raw = log return event, nil } -// BridgeNewWalletRegisteredIterator is returned from FilterNewWalletRegistered and is used to iterate over the raw logs and unpacked data for NewWalletRegistered events raised by the Bridge contract. -type BridgeNewWalletRegisteredIterator struct { - Event *BridgeNewWalletRegistered // Event containing the contract specifics and raw log +// BridgeP2TRFraudRouterSetIterator is returned from FilterP2TRFraudRouterSet and is used to iterate over the raw logs and unpacked data for P2TRFraudRouterSet events raised by the Bridge contract. +type BridgeP2TRFraudRouterSetIterator struct { + Event *BridgeP2TRFraudRouterSet // 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 @@ -4286,7 +5872,7 @@ type BridgeNewWalletRegisteredIterator struct { // 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 *BridgeNewWalletRegisteredIterator) Next() bool { +func (it *BridgeP2TRFraudRouterSetIterator) Next() bool { // If the iterator failed, stop iterating if it.fail != nil { return false @@ -4295,7 +5881,7 @@ func (it *BridgeNewWalletRegisteredIterator) Next() bool { if it.done { select { case log := <-it.logs: - it.Event = new(BridgeNewWalletRegistered) + it.Event = new(BridgeP2TRFraudRouterSet) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -4310,7 +5896,7 @@ func (it *BridgeNewWalletRegisteredIterator) Next() bool { // Iterator still in progress, wait for either a data or an error event select { case log := <-it.logs: - it.Event = new(BridgeNewWalletRegistered) + it.Event = new(BridgeP2TRFraudRouterSet) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -4326,60 +5912,41 @@ func (it *BridgeNewWalletRegisteredIterator) Next() bool { } // Error returns any retrieval or parsing error occurred during filtering. -func (it *BridgeNewWalletRegisteredIterator) Error() error { +func (it *BridgeP2TRFraudRouterSetIterator) Error() error { return it.fail } // Close terminates the iteration process, releasing any pending underlying // resources. -func (it *BridgeNewWalletRegisteredIterator) Close() error { +func (it *BridgeP2TRFraudRouterSetIterator) Close() error { it.sub.Unsubscribe() return nil } -// BridgeNewWalletRegistered represents a NewWalletRegistered event raised by the Bridge contract. -type BridgeNewWalletRegistered struct { - EcdsaWalletID [32]byte - WalletPubKeyHash [20]byte - Raw types.Log // Blockchain specific contextual infos +// BridgeP2TRFraudRouterSet represents a P2TRFraudRouterSet event raised by the Bridge contract. +type BridgeP2TRFraudRouterSet struct { + P2trFraudRouter common.Address + Raw types.Log // Blockchain specific contextual infos } -// FilterNewWalletRegistered is a free log retrieval operation binding the contract event 0x2dbb47dce81d6b11cca1f1e3b10143d6f7e1e7e92d2dd9aacbb1f875379d308e. +// FilterP2TRFraudRouterSet is a free log retrieval operation binding the contract event 0x083fc37b87ed978d63df891a75b6e9ab20e73e33ae9fcab66416b8d014ceee54. // -// Solidity: event NewWalletRegistered(bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) -func (_Bridge *BridgeFilterer) FilterNewWalletRegistered(opts *bind.FilterOpts, ecdsaWalletID [][32]byte, walletPubKeyHash [][20]byte) (*BridgeNewWalletRegisteredIterator, error) { - - var ecdsaWalletIDRule []interface{} - for _, ecdsaWalletIDItem := range ecdsaWalletID { - ecdsaWalletIDRule = append(ecdsaWalletIDRule, ecdsaWalletIDItem) - } - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) - } +// Solidity: event P2TRFraudRouterSet(address p2trFraudRouter) +func (_Bridge *BridgeFilterer) FilterP2TRFraudRouterSet(opts *bind.FilterOpts) (*BridgeP2TRFraudRouterSetIterator, error) { - logs, sub, err := _Bridge.contract.FilterLogs(opts, "NewWalletRegistered", ecdsaWalletIDRule, walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.FilterLogs(opts, "P2TRFraudRouterSet") if err != nil { return nil, err } - return &BridgeNewWalletRegisteredIterator{contract: _Bridge.contract, event: "NewWalletRegistered", logs: logs, sub: sub}, nil + return &BridgeP2TRFraudRouterSetIterator{contract: _Bridge.contract, event: "P2TRFraudRouterSet", logs: logs, sub: sub}, nil } -// WatchNewWalletRegistered is a free log subscription operation binding the contract event 0x2dbb47dce81d6b11cca1f1e3b10143d6f7e1e7e92d2dd9aacbb1f875379d308e. +// WatchP2TRFraudRouterSet is a free log subscription operation binding the contract event 0x083fc37b87ed978d63df891a75b6e9ab20e73e33ae9fcab66416b8d014ceee54. // -// Solidity: event NewWalletRegistered(bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) -func (_Bridge *BridgeFilterer) WatchNewWalletRegistered(opts *bind.WatchOpts, sink chan<- *BridgeNewWalletRegistered, ecdsaWalletID [][32]byte, walletPubKeyHash [][20]byte) (event.Subscription, error) { - - var ecdsaWalletIDRule []interface{} - for _, ecdsaWalletIDItem := range ecdsaWalletID { - ecdsaWalletIDRule = append(ecdsaWalletIDRule, ecdsaWalletIDItem) - } - var walletPubKeyHashRule []interface{} - for _, walletPubKeyHashItem := range walletPubKeyHash { - walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) - } +// Solidity: event P2TRFraudRouterSet(address p2trFraudRouter) +func (_Bridge *BridgeFilterer) WatchP2TRFraudRouterSet(opts *bind.WatchOpts, sink chan<- *BridgeP2TRFraudRouterSet) (event.Subscription, error) { - logs, sub, err := _Bridge.contract.WatchLogs(opts, "NewWalletRegistered", ecdsaWalletIDRule, walletPubKeyHashRule) + logs, sub, err := _Bridge.contract.WatchLogs(opts, "P2TRFraudRouterSet") if err != nil { return nil, err } @@ -4389,8 +5956,8 @@ func (_Bridge *BridgeFilterer) WatchNewWalletRegistered(opts *bind.WatchOpts, si select { case log := <-logs: // New log arrived, parse the event and forward to the user - event := new(BridgeNewWalletRegistered) - if err := _Bridge.contract.UnpackLog(event, "NewWalletRegistered", log); err != nil { + event := new(BridgeP2TRFraudRouterSet) + if err := _Bridge.contract.UnpackLog(event, "P2TRFraudRouterSet", log); err != nil { return err } event.Raw = log @@ -4411,21 +5978,21 @@ func (_Bridge *BridgeFilterer) WatchNewWalletRegistered(opts *bind.WatchOpts, si }), nil } -// ParseNewWalletRegistered is a log parse operation binding the contract event 0x2dbb47dce81d6b11cca1f1e3b10143d6f7e1e7e92d2dd9aacbb1f875379d308e. +// ParseP2TRFraudRouterSet is a log parse operation binding the contract event 0x083fc37b87ed978d63df891a75b6e9ab20e73e33ae9fcab66416b8d014ceee54. // -// Solidity: event NewWalletRegistered(bytes32 indexed ecdsaWalletID, bytes20 indexed walletPubKeyHash) -func (_Bridge *BridgeFilterer) ParseNewWalletRegistered(log types.Log) (*BridgeNewWalletRegistered, error) { - event := new(BridgeNewWalletRegistered) - if err := _Bridge.contract.UnpackLog(event, "NewWalletRegistered", log); err != nil { +// Solidity: event P2TRFraudRouterSet(address p2trFraudRouter) +func (_Bridge *BridgeFilterer) ParseP2TRFraudRouterSet(log types.Log) (*BridgeP2TRFraudRouterSet, error) { + event := new(BridgeP2TRFraudRouterSet) + if err := _Bridge.contract.UnpackLog(event, "P2TRFraudRouterSet", 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 +// 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 @@ -4439,7 +6006,7 @@ type BridgeNewWalletRequestedIterator struct { // 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 *BridgeNewWalletRequestedIterator) Next() bool { +func (it *BridgeRebateStakingSetIterator) Next() bool { // If the iterator failed, stop iterating if it.fail != nil { return false @@ -4448,7 +6015,7 @@ func (it *BridgeNewWalletRequestedIterator) Next() bool { if it.done { select { case log := <-it.logs: - it.Event = new(BridgeNewWalletRequested) + it.Event = new(BridgeRebateStakingSet) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -4463,7 +6030,7 @@ func (it *BridgeNewWalletRequestedIterator) Next() bool { // Iterator still in progress, wait for either a data or an error event select { case log := <-it.logs: - it.Event = new(BridgeNewWalletRequested) + it.Event = new(BridgeRebateStakingSet) if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { it.fail = err return false @@ -4479,40 +6046,41 @@ func (it *BridgeNewWalletRequestedIterator) Next() bool { } // Error returns any retrieval or parsing error occurred during filtering. -func (it *BridgeNewWalletRequestedIterator) Error() error { +func (it *BridgeRebateStakingSetIterator) Error() error { return it.fail } // Close terminates the iteration process, releasing any pending underlying // resources. -func (it *BridgeNewWalletRequestedIterator) Close() error { +func (it *BridgeRebateStakingSetIterator) Close() error { it.sub.Unsubscribe() return nil } -// BridgeNewWalletRequested represents a NewWalletRequested event raised by the Bridge contract. -type BridgeNewWalletRequested struct { - Raw types.Log // Blockchain specific contextual infos +// BridgeRebateStakingSet represents a RebateStakingSet event raised by the Bridge contract. +type BridgeRebateStakingSet struct { + RebateStaking common.Address + Raw types.Log // Blockchain specific contextual infos } -// FilterNewWalletRequested is a free log retrieval operation binding the contract event 0x31fecb80caf1e1128496dd5a6f1083ba29fd5fe64c3fe04e2d1b6f9cfc27d5a3. +// FilterRebateStakingSet is a free log retrieval operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. // -// Solidity: event NewWalletRequested() -func (_Bridge *BridgeFilterer) FilterNewWalletRequested(opts *bind.FilterOpts) (*BridgeNewWalletRequestedIterator, error) { +// Solidity: event RebateStakingSet(address rebateStaking) +func (_Bridge *BridgeFilterer) FilterRebateStakingSet(opts *bind.FilterOpts) (*BridgeRebateStakingSetIterator, error) { - logs, sub, err := _Bridge.contract.FilterLogs(opts, "NewWalletRequested") + logs, sub, err := _Bridge.contract.FilterLogs(opts, "RebateStakingSet") if err != nil { return nil, err } - return &BridgeNewWalletRequestedIterator{contract: _Bridge.contract, event: "NewWalletRequested", logs: logs, sub: sub}, nil + return &BridgeRebateStakingSetIterator{contract: _Bridge.contract, event: "RebateStakingSet", logs: logs, sub: sub}, nil } -// WatchNewWalletRequested is a free log subscription operation binding the contract event 0x31fecb80caf1e1128496dd5a6f1083ba29fd5fe64c3fe04e2d1b6f9cfc27d5a3. +// WatchRebateStakingSet is a free log subscription operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. // -// Solidity: event NewWalletRequested() -func (_Bridge *BridgeFilterer) WatchNewWalletRequested(opts *bind.WatchOpts, sink chan<- *BridgeNewWalletRequested) (event.Subscription, error) { +// 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, "NewWalletRequested") + logs, sub, err := _Bridge.contract.WatchLogs(opts, "RebateStakingSet") if err != nil { return nil, err } @@ -4522,8 +6090,8 @@ func (_Bridge *BridgeFilterer) WatchNewWalletRequested(opts *bind.WatchOpts, sin select { case log := <-logs: // New log arrived, parse the event and forward to the user - event := new(BridgeNewWalletRequested) - if err := _Bridge.contract.UnpackLog(event, "NewWalletRequested", log); err != nil { + event := new(BridgeRebateStakingSet) + if err := _Bridge.contract.UnpackLog(event, "RebateStakingSet", log); err != nil { return err } event.Raw = log @@ -4544,12 +6112,12 @@ func (_Bridge *BridgeFilterer) WatchNewWalletRequested(opts *bind.WatchOpts, sin }), nil } -// ParseNewWalletRequested is a log parse operation binding the contract event 0x31fecb80caf1e1128496dd5a6f1083ba29fd5fe64c3fe04e2d1b6f9cfc27d5a3. +// ParseRebateStakingSet is a log parse operation binding the contract event 0xd1d9d4e9f516cb983e81d2a124ec97cb8d4ff00637f2a7f3229eadbed84e2df6. // -// Solidity: event NewWalletRequested() -func (_Bridge *BridgeFilterer) ParseNewWalletRequested(log types.Log) (*BridgeNewWalletRequested, error) { - event := new(BridgeNewWalletRequested) - if err := _Bridge.contract.UnpackLog(event, "NewWalletRequested", log); err != nil { +// 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 @@ -5424,6 +6992,170 @@ func (_Bridge *BridgeFilterer) ParseSpvMaintainerStatusUpdated(log types.Log) (* return event, nil } +// BridgeTaprootDepositRevealedIterator is returned from FilterTaprootDepositRevealed and is used to iterate over the raw logs and unpacked data for TaprootDepositRevealed events raised by the Bridge contract. +type BridgeTaprootDepositRevealedIterator struct { + Event *BridgeTaprootDepositRevealed // 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 *BridgeTaprootDepositRevealedIterator) 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(BridgeTaprootDepositRevealed) + 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(BridgeTaprootDepositRevealed) + 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 *BridgeTaprootDepositRevealedIterator) Error() error { + return it.fail +} + +// Close terminates the iteration process, releasing any pending underlying +// resources. +func (it *BridgeTaprootDepositRevealedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +// BridgeTaprootDepositRevealed represents a TaprootDepositRevealed event raised by the Bridge contract. +type BridgeTaprootDepositRevealed struct { + FundingTxHash [32]byte + FundingOutputIndex uint32 + Depositor common.Address + Amount uint64 + BlindingFactor [8]byte + WalletPubKeyHash [20]byte + WalletXOnlyPublicKey [32]byte + RefundPubKeyHash [20]byte + RefundXOnlyPublicKey [32]byte + RefundLocktime [4]byte + Vault common.Address + Raw types.Log // Blockchain specific contextual infos +} + +// FilterTaprootDepositRevealed is a free log retrieval operation binding the contract event 0x50a25b08826caa8347dba14b236afbf87a8988553a910cbc953f1a53585d94cf. +// +// Solidity: event TaprootDepositRevealed(bytes32 fundingTxHash, uint32 fundingOutputIndex, address indexed depositor, uint64 amount, bytes8 blindingFactor, bytes20 indexed walletPubKeyHash, bytes32 walletXOnlyPublicKey, bytes20 refundPubKeyHash, bytes32 refundXOnlyPublicKey, bytes4 refundLocktime, address vault) +func (_Bridge *BridgeFilterer) FilterTaprootDepositRevealed(opts *bind.FilterOpts, depositor []common.Address, walletPubKeyHash [][20]byte) (*BridgeTaprootDepositRevealedIterator, error) { + + var depositorRule []interface{} + for _, depositorItem := range depositor { + depositorRule = append(depositorRule, depositorItem) + } + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _Bridge.contract.FilterLogs(opts, "TaprootDepositRevealed", depositorRule, walletPubKeyHashRule) + if err != nil { + return nil, err + } + return &BridgeTaprootDepositRevealedIterator{contract: _Bridge.contract, event: "TaprootDepositRevealed", logs: logs, sub: sub}, nil +} + +// WatchTaprootDepositRevealed is a free log subscription operation binding the contract event 0x50a25b08826caa8347dba14b236afbf87a8988553a910cbc953f1a53585d94cf. +// +// Solidity: event TaprootDepositRevealed(bytes32 fundingTxHash, uint32 fundingOutputIndex, address indexed depositor, uint64 amount, bytes8 blindingFactor, bytes20 indexed walletPubKeyHash, bytes32 walletXOnlyPublicKey, bytes20 refundPubKeyHash, bytes32 refundXOnlyPublicKey, bytes4 refundLocktime, address vault) +func (_Bridge *BridgeFilterer) WatchTaprootDepositRevealed(opts *bind.WatchOpts, sink chan<- *BridgeTaprootDepositRevealed, depositor []common.Address, walletPubKeyHash [][20]byte) (event.Subscription, error) { + + var depositorRule []interface{} + for _, depositorItem := range depositor { + depositorRule = append(depositorRule, depositorItem) + } + + var walletPubKeyHashRule []interface{} + for _, walletPubKeyHashItem := range walletPubKeyHash { + walletPubKeyHashRule = append(walletPubKeyHashRule, walletPubKeyHashItem) + } + + logs, sub, err := _Bridge.contract.WatchLogs(opts, "TaprootDepositRevealed", depositorRule, 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(BridgeTaprootDepositRevealed) + if err := _Bridge.contract.UnpackLog(event, "TaprootDepositRevealed", 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 +} + +// ParseTaprootDepositRevealed is a log parse operation binding the contract event 0x50a25b08826caa8347dba14b236afbf87a8988553a910cbc953f1a53585d94cf. +// +// Solidity: event TaprootDepositRevealed(bytes32 fundingTxHash, uint32 fundingOutputIndex, address indexed depositor, uint64 amount, bytes8 blindingFactor, bytes20 indexed walletPubKeyHash, bytes32 walletXOnlyPublicKey, bytes20 refundPubKeyHash, bytes32 refundXOnlyPublicKey, bytes4 refundLocktime, address vault) +func (_Bridge *BridgeFilterer) ParseTaprootDepositRevealed(log types.Log) (*BridgeTaprootDepositRevealed, error) { + event := new(BridgeTaprootDepositRevealed) + if err := _Bridge.contract.UnpackLog(event, "TaprootDepositRevealed", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + // BridgeTreasuryUpdatedIterator is returned from FilterTreasuryUpdated and is used to iterate over the raw logs and unpacked data for TreasuryUpdated events raised by the Bridge contract. type BridgeTreasuryUpdatedIterator struct { Event *BridgeTreasuryUpdated // Event containing the contract specifics and raw log diff --git a/pkg/chain/ethereum/tbtc/gen/abi/WalletProposalValidator.go b/pkg/chain/ethereum/tbtc/gen/abi/WalletProposalValidator.go index ed86d98785..21f79319ba 100644 --- a/pkg/chain/ethereum/tbtc/gen/abi/WalletProposalValidator.go +++ b/pkg/chain/ethereum/tbtc/gen/abi/WalletProposalValidator.go @@ -95,9 +95,20 @@ type WalletProposalValidatorRedemptionProposal struct { RedemptionTxFee *big.Int } +// WalletProposalValidatorTaprootDepositExtraInfo is an auto generated low-level Go binding around an user-defined struct. +type WalletProposalValidatorTaprootDepositExtraInfo struct { + FundingTx BitcoinTxInfo2 + BlindingFactor [8]byte + WalletPubKeyHash [20]byte + WalletXOnlyPublicKey [32]byte + RefundPubKeyHash [20]byte + RefundXOnlyPublicKey [32]byte + RefundLocktime [4]byte +} + // WalletProposalValidatorMetaData contains all meta data concerning the WalletProposalValidator contract. var WalletProposalValidatorMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"contractBridge\",\"name\":\"_bridge\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"DEPOSIT_MIN_AGE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_REFUND_SAFETY_MARGIN\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_SWEEP_MAX_SIZE\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_MAX_SIZE\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_REQUEST_MIN_AGE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_REQUEST_TIMEOUT_SAFETY_MARGIN\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bridge\",\"outputs\":[{\"internalType\":\"contractBridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"}],\"internalType\":\"structWalletProposalValidator.DepositKey[]\",\"name\":\"depositsKeys\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"sweepTxFee\",\"type\":\"uint256\"},{\"internalType\":\"uint256[]\",\"name\":\"depositsRevealBlocks\",\"type\":\"uint256[]\"}],\"internalType\":\"structWalletProposalValidator.DepositSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"},{\"components\":[{\"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\"},{\"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\":\"structWalletProposalValidator.DepositExtraInfo[]\",\"name\":\"depositsExtraInfo\",\"type\":\"tuple[]\"}],\"name\":\"validateDepositSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"internalType\":\"structWalletProposalValidator.HeartbeatProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateHeartbeatProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint256\",\"name\":\"movedFundsSweepTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.MovedFundsSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateMovedFundsSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"},{\"internalType\":\"uint256\",\"name\":\"movingFundsTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.MovingFundsProposal\",\"name\":\"proposal\",\"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\":\"walletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"validateMovingFundsProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes[]\",\"name\":\"redeemersOutputScripts\",\"type\":\"bytes[]\"},{\"internalType\":\"uint256\",\"name\":\"redemptionTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.RedemptionProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateRedemptionProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + ABI: "[{\"inputs\":[{\"internalType\":\"contractBridge\",\"name\":\"_bridge\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"DEPOSIT_MIN_AGE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_REFUND_SAFETY_MARGIN\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"DEPOSIT_SWEEP_MAX_SIZE\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_MAX_SIZE\",\"outputs\":[{\"internalType\":\"uint16\",\"name\":\"\",\"type\":\"uint16\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_REQUEST_MIN_AGE\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"REDEMPTION_REQUEST_TIMEOUT_SAFETY_MARGIN\",\"outputs\":[{\"internalType\":\"uint32\",\"name\":\"\",\"type\":\"uint32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"bridge\",\"outputs\":[{\"internalType\":\"contractBridge\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"}],\"internalType\":\"structWalletProposalValidator.DepositKey[]\",\"name\":\"depositsKeys\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"sweepTxFee\",\"type\":\"uint256\"},{\"internalType\":\"uint256[]\",\"name\":\"depositsRevealBlocks\",\"type\":\"uint256[]\"}],\"internalType\":\"structWalletProposalValidator.DepositSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"},{\"components\":[{\"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\"},{\"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\":\"structWalletProposalValidator.DepositExtraInfo[]\",\"name\":\"depositsExtraInfo\",\"type\":\"tuple[]\"}],\"name\":\"validateDepositSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes\",\"name\":\"message\",\"type\":\"bytes\"}],\"internalType\":\"structWalletProposalValidator.HeartbeatProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateHeartbeatProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes32\",\"name\":\"movingFundsTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"movingFundsTxOutputIndex\",\"type\":\"uint32\"},{\"internalType\":\"uint256\",\"name\":\"movedFundsSweepTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.MovedFundsSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateMovedFundsSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes20[]\",\"name\":\"targetWallets\",\"type\":\"bytes20[]\"},{\"internalType\":\"uint256\",\"name\":\"movingFundsTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.MovingFundsProposal\",\"name\":\"proposal\",\"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\":\"walletMainUtxo\",\"type\":\"tuple\"}],\"name\":\"validateMovingFundsProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes[]\",\"name\":\"redeemersOutputScripts\",\"type\":\"bytes[]\"},{\"internalType\":\"uint256\",\"name\":\"redemptionTxFee\",\"type\":\"uint256\"}],\"internalType\":\"structWalletProposalValidator.RedemptionProposal\",\"name\":\"proposal\",\"type\":\"tuple\"}],\"name\":\"validateRedemptionProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"fundingTxHash\",\"type\":\"bytes32\"},{\"internalType\":\"uint32\",\"name\":\"fundingOutputIndex\",\"type\":\"uint32\"}],\"internalType\":\"structWalletProposalValidator.DepositKey[]\",\"name\":\"depositsKeys\",\"type\":\"tuple[]\"},{\"internalType\":\"uint256\",\"name\":\"sweepTxFee\",\"type\":\"uint256\"},{\"internalType\":\"uint256[]\",\"name\":\"depositsRevealBlocks\",\"type\":\"uint256[]\"}],\"internalType\":\"structWalletProposalValidator.DepositSweepProposal\",\"name\":\"proposal\",\"type\":\"tuple\"},{\"components\":[{\"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\"},{\"internalType\":\"bytes8\",\"name\":\"blindingFactor\",\"type\":\"bytes8\"},{\"internalType\":\"bytes20\",\"name\":\"walletPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes32\",\"name\":\"walletXOnlyPublicKey\",\"type\":\"bytes32\"},{\"internalType\":\"bytes20\",\"name\":\"refundPubKeyHash\",\"type\":\"bytes20\"},{\"internalType\":\"bytes32\",\"name\":\"refundXOnlyPublicKey\",\"type\":\"bytes32\"},{\"internalType\":\"bytes4\",\"name\":\"refundLocktime\",\"type\":\"bytes4\"}],\"internalType\":\"structWalletProposalValidator.TaprootDepositExtraInfo[]\",\"name\":\"depositsExtraInfo\",\"type\":\"tuple[]\"}],\"name\":\"validateTaprootDepositSweepProposal\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", } // WalletProposalValidatorABI is the input ABI used to generate the binding from. @@ -617,3 +628,34 @@ func (_WalletProposalValidator *WalletProposalValidatorSession) ValidateRedempti func (_WalletProposalValidator *WalletProposalValidatorCallerSession) ValidateRedemptionProposal(proposal WalletProposalValidatorRedemptionProposal) (bool, error) { return _WalletProposalValidator.Contract.ValidateRedemptionProposal(&_WalletProposalValidator.CallOpts, proposal) } + +// ValidateTaprootDepositSweepProposal is a free data retrieval call binding the contract method 0xb1782302. +// +// Solidity: function validateTaprootDepositSweepProposal((bytes20,(bytes32,uint32)[],uint256,uint256[]) proposal, ((bytes4,bytes,bytes,bytes4),bytes8,bytes20,bytes32,bytes20,bytes32,bytes4)[] depositsExtraInfo) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorCaller) ValidateTaprootDepositSweepProposal(opts *bind.CallOpts, proposal WalletProposalValidatorDepositSweepProposal, depositsExtraInfo []WalletProposalValidatorTaprootDepositExtraInfo) (bool, error) { + var out []interface{} + err := _WalletProposalValidator.contract.Call(opts, &out, "validateTaprootDepositSweepProposal", proposal, depositsExtraInfo) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// ValidateTaprootDepositSweepProposal is a free data retrieval call binding the contract method 0xb1782302. +// +// Solidity: function validateTaprootDepositSweepProposal((bytes20,(bytes32,uint32)[],uint256,uint256[]) proposal, ((bytes4,bytes,bytes,bytes4),bytes8,bytes20,bytes32,bytes20,bytes32,bytes4)[] depositsExtraInfo) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorSession) ValidateTaprootDepositSweepProposal(proposal WalletProposalValidatorDepositSweepProposal, depositsExtraInfo []WalletProposalValidatorTaprootDepositExtraInfo) (bool, error) { + return _WalletProposalValidator.Contract.ValidateTaprootDepositSweepProposal(&_WalletProposalValidator.CallOpts, proposal, depositsExtraInfo) +} + +// ValidateTaprootDepositSweepProposal is a free data retrieval call binding the contract method 0xb1782302. +// +// Solidity: function validateTaprootDepositSweepProposal((bytes20,(bytes32,uint32)[],uint256,uint256[]) proposal, ((bytes4,bytes,bytes,bytes4),bytes8,bytes20,bytes32,bytes20,bytes32,bytes4)[] depositsExtraInfo) view returns(bool) +func (_WalletProposalValidator *WalletProposalValidatorCallerSession) ValidateTaprootDepositSweepProposal(proposal WalletProposalValidatorDepositSweepProposal, depositsExtraInfo []WalletProposalValidatorTaprootDepositExtraInfo) (bool, error) { + return _WalletProposalValidator.Contract.ValidateTaprootDepositSweepProposal(&_WalletProposalValidator.CallOpts, proposal, depositsExtraInfo) +} diff --git a/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go b/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go index f7a5944669..058deabc58 100644 --- a/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/cmd/Bridge.go @@ -52,31 +52,38 @@ func init() { } BridgeCommand.AddCommand( + bActiveWalletIDCommand(), bActiveWalletPubKeyHashCommand(), bContractReferencesCommand(), bDepositParametersCommand(), bDepositsCommand(), - bFraudChallengesCommand(), + bEcdsaFraudRouterCommand(), + bEcdsaRetiredCommand(), bFraudParametersCommand(), + bFrostLifecycleContextCommand(), + bGetRebateStakingCommand(), bGetRedemptionWatchtowerCommand(), bGovernanceCommand(), bIsVaultTrustedCommand(), bLiveWalletsCountCommand(), bMovedFundsSweepRequestsCommand(), bMovingFundsParametersCommand(), + bP2trFraudRouterCommand(), bPendingRedemptionsCommand(), bRedemptionParametersCommand(), bSpentMainUTXOsCommand(), bTimedOutRedemptionsCommand(), bTreasuryCommand(), bTxProofDifficultyFactorCommand(), + bWalletIDCommand(), bWalletParametersCommand(), + bWalletPubKeyHashForWalletIDCommand(), bWalletsCommand(), - bDefeatFraudChallengeCommand(), - bDefeatFraudChallengeWithHeartbeatCommand(), - bEcdsaWalletCreatedCallbackCommand(), + bWalletsByWalletIDCommand(), bEcdsaWalletHeartbeatFailedCallbackCommand(), + bFrostWalletCreatedCallbackCommand(), bInitializeCommand(), + bInitializeV2FixVaultZeroDepositCommand(), bNotifyMovingFundsBelowDustCommand(), bNotifyRedemptionVetoCommand(), bNotifyWalletCloseableCommand(), @@ -85,13 +92,20 @@ func init() { bRequestNewWalletCommand(), bRequestRedemptionCommand(), bResetMovingFundsTimeoutCommand(), + bRetireEcdsaCommand(), bRevealDepositCommand(), bRevealDepositWithExtraDataCommand(), + bRevealTaprootDepositCommand(), + bRevealTaprootDepositWithExtraDataCommand(), + bSetEcdsaFraudRouterCommand(), + bSetFrostWalletRegistryCommand(), + bSetLifecycleRouterCommand(), + bSetP2TRFraudRouterCommand(), + bSetRebateStakingCommand(), bSetRedemptionWatchtowerCommand(), bSetSpvMaintainerStatusCommand(), bSetVaultStatusCommand(), bSubmitDepositSweepProofCommand(), - bSubmitFraudChallengeCommand(), bSubmitMovedFundsSweepProofCommand(), bSubmitMovingFundsProofCommand(), bSubmitRedemptionProofCommand(), @@ -109,6 +123,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", @@ -254,12 +302,12 @@ func bDeposits(c *cobra.Command, args []string) error { return nil } -func bFraudChallengesCommand() *cobra.Command { +func bEcdsaFraudRouterCommand() *cobra.Command { c := &cobra.Command{ - Use: "fraud-challenges [arg_challengeKey]", - Short: "Calls the view method fraudChallenges on the Bridge contract.", - Args: cmd.ArgCountChecker(1), - RunE: bFraudChallenges, + Use: "ecdsa-fraud-router", + Short: "Calls the view method ecdsaFraudRouter on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bEcdsaFraudRouter, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -269,22 +317,47 @@ func bFraudChallengesCommand() *cobra.Command { return c } -func bFraudChallenges(c *cobra.Command, args []string) error { +func bEcdsaFraudRouter(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_challengeKey, err := hexutil.DecodeBig(args[0]) + result, err := contract.EcdsaFraudRouterAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + +func bEcdsaRetiredCommand() *cobra.Command { + c := &cobra.Command{ + Use: "ecdsa-retired", + Short: "Calls the view method ecdsaRetired on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bEcdsaRetired, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bEcdsaRetired(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) if err != nil { - return fmt.Errorf( - "couldn't parse parameter arg_challengeKey, a uint256, from passed value %v", - args[0], - ) + return err } - result, err := contract.FraudChallengesAtBlock( - arg_challengeKey, + result, err := contract.EcdsaRetiredAtBlock( cmd.BlockFlagValue.Int, ) @@ -331,6 +404,83 @@ func bFraudParameters(c *cobra.Command, args []string) error { return nil } +func bFrostLifecycleContextCommand() *cobra.Command { + c := &cobra.Command{ + Use: "frost-lifecycle-context [arg_walletPubKeyHash]", + Short: "Calls the view method frostLifecycleContext on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bFrostLifecycleContext, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bFrostLifecycleContext(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.FrostLifecycleContextAtBlock( + arg_walletPubKeyHash, + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + 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", @@ -553,6 +703,40 @@ func bMovingFundsParameters(c *cobra.Command, args []string) error { return nil } +func bP2trFraudRouterCommand() *cobra.Command { + c := &cobra.Command{ + Use: "p2tr-fraud-router", + Short: "Calls the view method p2trFraudRouter on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bP2trFraudRouter, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + cmd.InitConstFlags(c) + + return c +} + +func bP2trFraudRouter(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + result, err := contract.P2trFraudRouterAtBlock( + cmd.BlockFlagValue.Int, + ) + + if err != nil { + return err + } + + cmd.PrintOutput(result) + + return nil +} + func bPendingRedemptionsCommand() *cobra.Command { c := &cobra.Command{ Use: "pending-redemptions [arg_redemptionKey]", @@ -784,12 +968,12 @@ func bTxProofDifficultyFactor(c *cobra.Command, args []string) error { return nil } -func bWalletParametersCommand() *cobra.Command { +func bWalletIDCommand() *cobra.Command { c := &cobra.Command{ - Use: "wallet-parameters", - Short: "Calls the view method walletParameters on the Bridge contract.", - Args: cmd.ArgCountChecker(0), - RunE: bWalletParameters, + Use: "wallet-i-d [arg_walletPubKeyHash]", + Short: "Calls the view method walletID on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bWalletID, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -799,13 +983,22 @@ func bWalletParametersCommand() *cobra.Command { return c } -func bWalletParameters(c *cobra.Command, args []string) error { +func bWalletID(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - result, err := contract.WalletParametersAtBlock( + 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, ) @@ -818,12 +1011,12 @@ func bWalletParameters(c *cobra.Command, args []string) error { return nil } -func bWalletsCommand() *cobra.Command { +func bWalletParametersCommand() *cobra.Command { c := &cobra.Command{ - Use: "wallets [arg_walletPubKeyHash]", - Short: "Calls the view method wallets on the Bridge contract.", - Args: cmd.ArgCountChecker(1), - RunE: bWallets, + Use: "wallet-parameters", + Short: "Calls the view method walletParameters on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bWalletParameters, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -833,22 +1026,13 @@ func bWalletsCommand() *cobra.Command { return c } -func bWallets(c *cobra.Command, args []string) error { +func bWalletParameters(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.WalletsAtBlock( - arg_walletPubKeyHash, + result, err := contract.WalletParametersAtBlock( cmd.BlockFlagValue.Int, ) @@ -861,171 +1045,143 @@ func bWallets(c *cobra.Command, args []string) error { return nil } -/// ------------------- Non-const methods ------------------- - -func bDefeatFraudChallengeCommand() *cobra.Command { +func bWalletPubKeyHashForWalletIDCommand() *cobra.Command { c := &cobra.Command{ - Use: "defeat-fraud-challenge [arg_walletPublicKey] [arg_preimage] [arg_witness]", - Short: "Calls the nonpayable method defeatFraudChallenge on the Bridge contract.", - Args: cmd.ArgCountChecker(3), - RunE: bDefeatFraudChallenge, + 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, } - c.PreRunE = cmd.NonConstArgsChecker - cmd.InitNonConstFlags(c) + cmd.InitConstFlags(c) return c } -func bDefeatFraudChallenge(c *cobra.Command, args []string) error { +func bWalletPubKeyHashForWalletID(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_walletPublicKey, err := hexutil.Decode(args[0]) + arg_walletId, err := decode.ParseBytes32(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_walletPublicKey, a bytes, from passed value %v", + "couldn't parse parameter arg_walletId, a bytes32, from passed value %v", args[0], ) } - arg_preimage, err := hexutil.Decode(args[1]) - if err != nil { - return fmt.Errorf( - "couldn't parse parameter arg_preimage, a bytes, from passed value %v", - args[1], - ) - } - arg_witness, err := strconv.ParseBool(args[2]) - if err != nil { - return fmt.Errorf( - "couldn't parse parameter arg_witness, a bool, from passed value %v", - args[2], - ) - } - var ( - transaction *types.Transaction + result, err := contract.WalletPubKeyHashForWalletIDAtBlock( + arg_walletId, + cmd.BlockFlagValue.Int, ) - if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { - // Do a regular submission. Take payable into account. - transaction, err = contract.DefeatFraudChallenge( - arg_walletPublicKey, - arg_preimage, - arg_witness, - ) - if err != nil { - return err - } - - cmd.PrintOutput(transaction.Hash()) - } else { - // Do a call. - err = contract.CallDefeatFraudChallenge( - arg_walletPublicKey, - arg_preimage, - arg_witness, - 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", - ) + if err != nil { + return err } + cmd.PrintOutput(result) + return nil } -func bDefeatFraudChallengeWithHeartbeatCommand() *cobra.Command { +func bWalletsCommand() *cobra.Command { c := &cobra.Command{ - Use: "defeat-fraud-challenge-with-heartbeat [arg_walletPublicKey] [arg_heartbeatMessage]", - Short: "Calls the nonpayable method defeatFraudChallengeWithHeartbeat on the Bridge contract.", - Args: cmd.ArgCountChecker(2), - RunE: bDefeatFraudChallengeWithHeartbeat, + Use: "wallets [arg_walletPubKeyHash]", + Short: "Calls the view method wallets on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bWallets, SilenceUsage: true, DisableFlagsInUseLine: true, } - c.PreRunE = cmd.NonConstArgsChecker - cmd.InitNonConstFlags(c) + cmd.InitConstFlags(c) return c } -func bDefeatFraudChallengeWithHeartbeat(c *cobra.Command, args []string) error { +func bWallets(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_walletPublicKey, err := hexutil.Decode(args[0]) + arg_walletPubKeyHash, err := decode.ParseBytes20(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_walletPublicKey, a bytes, from passed value %v", + "couldn't parse parameter arg_walletPubKeyHash, a bytes20, from passed value %v", args[0], ) } - arg_heartbeatMessage, err := hexutil.Decode(args[1]) - if err != nil { - return fmt.Errorf( - "couldn't parse parameter arg_heartbeatMessage, a bytes, from passed value %v", - args[1], - ) - } - var ( - transaction *types.Transaction + result, err := contract.WalletsAtBlock( + arg_walletPubKeyHash, + cmd.BlockFlagValue.Int, ) - if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { - // Do a regular submission. Take payable into account. - transaction, err = contract.DefeatFraudChallengeWithHeartbeat( - arg_walletPublicKey, - arg_heartbeatMessage, - ) - if err != nil { - return err - } + if err != nil { + return err + } - cmd.PrintOutput(transaction.Hash()) - } else { - // Do a call. - err = contract.CallDefeatFraudChallengeWithHeartbeat( - arg_walletPublicKey, - arg_heartbeatMessage, - cmd.BlockFlagValue.Int, - ) - if err != nil { - return err - } + cmd.PrintOutput(result) - cmd.PrintOutput("success") + return nil +} - cmd.PrintOutput( - "the transaction was not submitted to the chain; " + - "please add the `--submit` flag", +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 } -func bEcdsaWalletCreatedCallbackCommand() *cobra.Command { +/// ------------------- Non-const methods ------------------- + +func bEcdsaWalletHeartbeatFailedCallbackCommand() *cobra.Command { c := &cobra.Command{ - Use: "ecdsa-wallet-created-callback [arg_ecdsaWalletID] [arg_publicKeyX] [arg_publicKeyY]", - Short: "Calls the nonpayable method ecdsaWalletCreatedCallback on the Bridge contract.", + Use: "ecdsa-wallet-heartbeat-failed-callback [arg0] [arg_publicKeyX] [arg_publicKeyY]", + Short: "Calls the nonpayable method ecdsaWalletHeartbeatFailedCallback on the Bridge contract.", Args: cmd.ArgCountChecker(3), - RunE: bEcdsaWalletCreatedCallback, + RunE: bEcdsaWalletHeartbeatFailedCallback, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -1036,16 +1192,16 @@ func bEcdsaWalletCreatedCallbackCommand() *cobra.Command { return c } -func bEcdsaWalletCreatedCallback(c *cobra.Command, args []string) error { +func bEcdsaWalletHeartbeatFailedCallback(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_ecdsaWalletID, err := decode.ParseBytes32(args[0]) + arg0, err := decode.ParseBytes32(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_ecdsaWalletID, a bytes32, from passed value %v", + "couldn't parse parameter arg0, a bytes32, from passed value %v", args[0], ) } @@ -1070,8 +1226,8 @@ func bEcdsaWalletCreatedCallback(c *cobra.Command, args []string) error { if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { // Do a regular submission. Take payable into account. - transaction, err = contract.EcdsaWalletCreatedCallback( - arg_ecdsaWalletID, + transaction, err = contract.EcdsaWalletHeartbeatFailedCallback( + arg0, arg_publicKeyX, arg_publicKeyY, ) @@ -1082,8 +1238,8 @@ func bEcdsaWalletCreatedCallback(c *cobra.Command, args []string) error { cmd.PrintOutput(transaction.Hash()) } else { // Do a call. - err = contract.CallEcdsaWalletCreatedCallback( - arg_ecdsaWalletID, + err = contract.CallEcdsaWalletHeartbeatFailedCallback( + arg0, arg_publicKeyX, arg_publicKeyY, cmd.BlockFlagValue.Int, @@ -1103,12 +1259,12 @@ func bEcdsaWalletCreatedCallback(c *cobra.Command, args []string) error { return nil } -func bEcdsaWalletHeartbeatFailedCallbackCommand() *cobra.Command { +func bFrostWalletCreatedCallbackCommand() *cobra.Command { c := &cobra.Command{ - Use: "ecdsa-wallet-heartbeat-failed-callback [arg0] [arg_publicKeyX] [arg_publicKeyY]", - Short: "Calls the nonpayable method ecdsaWalletHeartbeatFailedCallback on the Bridge contract.", - Args: cmd.ArgCountChecker(3), - RunE: bEcdsaWalletHeartbeatFailedCallback, + Use: "frost-wallet-created-callback [arg_xOnlyOutputKey]", + Short: "Calls the nonpayable method frostWalletCreatedCallback on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bFrostWalletCreatedCallback, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -1119,33 +1275,19 @@ func bEcdsaWalletHeartbeatFailedCallbackCommand() *cobra.Command { return c } -func bEcdsaWalletHeartbeatFailedCallback(c *cobra.Command, args []string) error { +func bFrostWalletCreatedCallback(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg0, err := decode.ParseBytes32(args[0]) + arg_xOnlyOutputKey, err := decode.ParseBytes32(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg0, a bytes32, from passed value %v", + "couldn't parse parameter arg_xOnlyOutputKey, a bytes32, from passed value %v", args[0], ) } - arg_publicKeyX, err := decode.ParseBytes32(args[1]) - if err != nil { - return fmt.Errorf( - "couldn't parse parameter arg_publicKeyX, a bytes32, from passed value %v", - args[1], - ) - } - arg_publicKeyY, err := decode.ParseBytes32(args[2]) - if err != nil { - return fmt.Errorf( - "couldn't parse parameter arg_publicKeyY, a bytes32, from passed value %v", - args[2], - ) - } var ( transaction *types.Transaction @@ -1153,10 +1295,8 @@ func bEcdsaWalletHeartbeatFailedCallback(c *cobra.Command, args []string) error if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { // Do a regular submission. Take payable into account. - transaction, err = contract.EcdsaWalletHeartbeatFailedCallback( - arg0, - arg_publicKeyX, - arg_publicKeyY, + transaction, err = contract.FrostWalletCreatedCallback( + arg_xOnlyOutputKey, ) if err != nil { return err @@ -1165,10 +1305,8 @@ func bEcdsaWalletHeartbeatFailedCallback(c *cobra.Command, args []string) error cmd.PrintOutput(transaction.Hash()) } else { // Do a call. - err = contract.CallEcdsaWalletHeartbeatFailedCallback( - arg0, - arg_publicKeyX, - arg_publicKeyY, + err = contract.CallFrostWalletCreatedCallback( + arg_xOnlyOutputKey, cmd.BlockFlagValue.Int, ) if err != nil { @@ -1296,6 +1434,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]", @@ -1879,6 +2071,60 @@ func bResetMovingFundsTimeout(c *cobra.Command, args []string) error { return nil } +func bRetireEcdsaCommand() *cobra.Command { + c := &cobra.Command{ + Use: "retire-ecdsa", + Short: "Calls the nonpayable method retireEcdsa on the Bridge contract.", + Args: cmd.ArgCountChecker(0), + RunE: bRetireEcdsa, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bRetireEcdsa(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.RetireEcdsa() + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallRetireEcdsa( + 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 bRevealDepositCommand() *cobra.Command { c := &cobra.Command{ Use: "reveal-deposit [arg_fundingTx_json] [arg_reveal_json]", @@ -2026,12 +2272,12 @@ func bRevealDepositWithExtraData(c *cobra.Command, args []string) error { return nil } -func bSetRedemptionWatchtowerCommand() *cobra.Command { +func bRevealTaprootDepositCommand() *cobra.Command { c := &cobra.Command{ - Use: "set-redemption-watchtower [arg_redemptionWatchtower]", - Short: "Calls the nonpayable method setRedemptionWatchtower on the Bridge contract.", - Args: cmd.ArgCountChecker(1), - RunE: bSetRedemptionWatchtower, + Use: "reveal-taproot-deposit [arg_fundingTx_json] [arg_reveal_json]", + Short: "Calls the nonpayable method revealTaprootDeposit on the Bridge contract.", + Args: cmd.ArgCountChecker(2), + RunE: bRevealTaprootDeposit, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -2042,18 +2288,20 @@ func bSetRedemptionWatchtowerCommand() *cobra.Command { return c } -func bSetRedemptionWatchtower(c *cobra.Command, args []string) error { +func bRevealTaprootDeposit(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_redemptionWatchtower, err := chainutil.AddressFromHex(args[0]) - if err != nil { - return fmt.Errorf( - "couldn't parse parameter arg_redemptionWatchtower, a address, from passed value %v", - args[0], - ) + arg_fundingTx_json := abi.BitcoinTxInfo{} + if err := json.Unmarshal([]byte(args[0]), &arg_fundingTx_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_fundingTx_json to abi.BitcoinTxInfo: %w", err) + } + + arg_reveal_json := abi.DepositTaprootDepositRevealInfo{} + if err := json.Unmarshal([]byte(args[1]), &arg_reveal_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_reveal_json to abi.DepositTaprootDepositRevealInfo: %w", err) } var ( @@ -2062,8 +2310,9 @@ func bSetRedemptionWatchtower(c *cobra.Command, args []string) error { if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { // Do a regular submission. Take payable into account. - transaction, err = contract.SetRedemptionWatchtower( - arg_redemptionWatchtower, + transaction, err = contract.RevealTaprootDeposit( + arg_fundingTx_json, + arg_reveal_json, ) if err != nil { return err @@ -2072,8 +2321,9 @@ func bSetRedemptionWatchtower(c *cobra.Command, args []string) error { cmd.PrintOutput(transaction.Hash()) } else { // Do a call. - err = contract.CallSetRedemptionWatchtower( - arg_redemptionWatchtower, + err = contract.CallRevealTaprootDeposit( + arg_fundingTx_json, + arg_reveal_json, cmd.BlockFlagValue.Int, ) if err != nil { @@ -2091,12 +2341,12 @@ func bSetRedemptionWatchtower(c *cobra.Command, args []string) error { return nil } -func bSetSpvMaintainerStatusCommand() *cobra.Command { +func bRevealTaprootDepositWithExtraDataCommand() *cobra.Command { c := &cobra.Command{ - Use: "set-spv-maintainer-status [arg_spvMaintainer] [arg_isTrusted]", - Short: "Calls the nonpayable method setSpvMaintainerStatus on the Bridge contract.", - Args: cmd.ArgCountChecker(2), - RunE: bSetSpvMaintainerStatus, + Use: "reveal-taproot-deposit-with-extra-data [arg_fundingTx_json] [arg_reveal_json] [arg_extraData]", + Short: "Calls the nonpayable method revealTaprootDepositWithExtraData on the Bridge contract.", + Args: cmd.ArgCountChecker(3), + RunE: bRevealTaprootDepositWithExtraData, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -2107,24 +2357,26 @@ func bSetSpvMaintainerStatusCommand() *cobra.Command { return c } -func bSetSpvMaintainerStatus(c *cobra.Command, args []string) error { +func bRevealTaprootDepositWithExtraData(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_spvMaintainer, err := chainutil.AddressFromHex(args[0]) - if err != nil { - return fmt.Errorf( - "couldn't parse parameter arg_spvMaintainer, a address, from passed value %v", - args[0], - ) + arg_fundingTx_json := abi.BitcoinTxInfo{} + if err := json.Unmarshal([]byte(args[0]), &arg_fundingTx_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_fundingTx_json to abi.BitcoinTxInfo: %w", err) } - arg_isTrusted, err := strconv.ParseBool(args[1]) + + arg_reveal_json := abi.DepositTaprootDepositRevealInfo{} + if err := json.Unmarshal([]byte(args[1]), &arg_reveal_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_reveal_json to abi.DepositTaprootDepositRevealInfo: %w", err) + } + arg_extraData, err := decode.ParseBytes32(args[2]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_isTrusted, a bool, from passed value %v", - args[1], + "couldn't parse parameter arg_extraData, a bytes32, from passed value %v", + args[2], ) } @@ -2134,9 +2386,10 @@ func bSetSpvMaintainerStatus(c *cobra.Command, args []string) error { if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { // Do a regular submission. Take payable into account. - transaction, err = contract.SetSpvMaintainerStatus( - arg_spvMaintainer, - arg_isTrusted, + transaction, err = contract.RevealTaprootDepositWithExtraData( + arg_fundingTx_json, + arg_reveal_json, + arg_extraData, ) if err != nil { return err @@ -2145,11 +2398,12 @@ func bSetSpvMaintainerStatus(c *cobra.Command, args []string) error { cmd.PrintOutput(transaction.Hash()) } else { // Do a call. - err = contract.CallSetSpvMaintainerStatus( - arg_spvMaintainer, - arg_isTrusted, - cmd.BlockFlagValue.Int, - ) + err = contract.CallRevealTaprootDepositWithExtraData( + arg_fundingTx_json, + arg_reveal_json, + arg_extraData, + cmd.BlockFlagValue.Int, + ) if err != nil { return err } @@ -2165,12 +2419,12 @@ func bSetSpvMaintainerStatus(c *cobra.Command, args []string) error { return nil } -func bSetVaultStatusCommand() *cobra.Command { +func bSetEcdsaFraudRouterCommand() *cobra.Command { c := &cobra.Command{ - Use: "set-vault-status [arg_vault] [arg_isTrusted]", - Short: "Calls the nonpayable method setVaultStatus on the Bridge contract.", - Args: cmd.ArgCountChecker(2), - RunE: bSetVaultStatus, + Use: "set-ecdsa-fraud-router [arg_ecdsaFraudRouter]", + Short: "Calls the nonpayable method setEcdsaFraudRouter on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bSetEcdsaFraudRouter, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -2181,24 +2435,82 @@ func bSetVaultStatusCommand() *cobra.Command { return c } -func bSetVaultStatus(c *cobra.Command, args []string) error { +func bSetEcdsaFraudRouter(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_vault, err := chainutil.AddressFromHex(args[0]) + arg_ecdsaFraudRouter, err := chainutil.AddressFromHex(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_vault, a address, from passed value %v", + "couldn't parse parameter arg_ecdsaFraudRouter, a address, from passed value %v", args[0], ) } - arg_isTrusted, err := strconv.ParseBool(args[1]) + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.SetEcdsaFraudRouter( + arg_ecdsaFraudRouter, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSetEcdsaFraudRouter( + arg_ecdsaFraudRouter, + 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 bSetFrostWalletRegistryCommand() *cobra.Command { + c := &cobra.Command{ + Use: "set-frost-wallet-registry [arg_frostWalletRegistry]", + Short: "Calls the nonpayable method setFrostWalletRegistry on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bSetFrostWalletRegistry, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSetFrostWalletRegistry(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_frostWalletRegistry, err := chainutil.AddressFromHex(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_isTrusted, a bool, from passed value %v", - args[1], + "couldn't parse parameter arg_frostWalletRegistry, a address, from passed value %v", + args[0], ) } @@ -2208,9 +2520,8 @@ func bSetVaultStatus(c *cobra.Command, args []string) error { if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { // Do a regular submission. Take payable into account. - transaction, err = contract.SetVaultStatus( - arg_vault, - arg_isTrusted, + transaction, err = contract.SetFrostWalletRegistry( + arg_frostWalletRegistry, ) if err != nil { return err @@ -2219,9 +2530,8 @@ func bSetVaultStatus(c *cobra.Command, args []string) error { cmd.PrintOutput(transaction.Hash()) } else { // Do a call. - err = contract.CallSetVaultStatus( - arg_vault, - arg_isTrusted, + err = contract.CallSetFrostWalletRegistry( + arg_frostWalletRegistry, cmd.BlockFlagValue.Int, ) if err != nil { @@ -2239,12 +2549,12 @@ func bSetVaultStatus(c *cobra.Command, args []string) error { return nil } -func bSubmitDepositSweepProofCommand() *cobra.Command { +func bSetLifecycleRouterCommand() *cobra.Command { c := &cobra.Command{ - Use: "submit-deposit-sweep-proof [arg_sweepTx_json] [arg_sweepProof_json] [arg_mainUtxo_json] [arg_vault]", - Short: "Calls the nonpayable method submitDepositSweepProof on the Bridge contract.", - Args: cmd.ArgCountChecker(4), - RunE: bSubmitDepositSweepProof, + Use: "set-lifecycle-router [arg_lifecycleRouter]", + Short: "Calls the nonpayable method setLifecycleRouter on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bSetLifecycleRouter, SilenceUsage: true, DisableFlagsInUseLine: true, } @@ -2255,31 +2565,82 @@ func bSubmitDepositSweepProofCommand() *cobra.Command { return c } -func bSubmitDepositSweepProof(c *cobra.Command, args []string) error { +func bSetLifecycleRouter(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_sweepTx_json := abi.BitcoinTxInfo{} - if err := json.Unmarshal([]byte(args[0]), &arg_sweepTx_json); err != nil { - return fmt.Errorf("failed to unmarshal arg_sweepTx_json to abi.BitcoinTxInfo: %w", err) + arg_lifecycleRouter, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_lifecycleRouter, a address, from passed value %v", + args[0], + ) } - arg_sweepProof_json := abi.BitcoinTxProof{} - if err := json.Unmarshal([]byte(args[1]), &arg_sweepProof_json); err != nil { - return fmt.Errorf("failed to unmarshal arg_sweepProof_json to abi.BitcoinTxProof: %w", err) + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.SetLifecycleRouter( + arg_lifecycleRouter, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSetLifecycleRouter( + arg_lifecycleRouter, + 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", + ) } - arg_mainUtxo_json := abi.BitcoinTxUTXO{} - if err := json.Unmarshal([]byte(args[2]), &arg_mainUtxo_json); err != nil { - return fmt.Errorf("failed to unmarshal arg_mainUtxo_json to abi.BitcoinTxUTXO: %w", err) + return nil +} + +func bSetP2TRFraudRouterCommand() *cobra.Command { + c := &cobra.Command{ + Use: "set-p2-t-r-fraud-router [arg_p2trFraudRouter]", + Short: "Calls the nonpayable method setP2TRFraudRouter on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bSetP2TRFraudRouter, + SilenceUsage: true, + DisableFlagsInUseLine: true, } - arg_vault, err := chainutil.AddressFromHex(args[3]) + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSetP2TRFraudRouter(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_p2trFraudRouter, err := chainutil.AddressFromHex(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_vault, a address, from passed value %v", - args[3], + "couldn't parse parameter arg_p2trFraudRouter, a address, from passed value %v", + args[0], ) } @@ -2289,11 +2650,8 @@ func bSubmitDepositSweepProof(c *cobra.Command, args []string) error { if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { // Do a regular submission. Take payable into account. - transaction, err = contract.SubmitDepositSweepProof( - arg_sweepTx_json, - arg_sweepProof_json, - arg_mainUtxo_json, - arg_vault, + transaction, err = contract.SetP2TRFraudRouter( + arg_p2trFraudRouter, ) if err != nil { return err @@ -2302,11 +2660,8 @@ func bSubmitDepositSweepProof(c *cobra.Command, args []string) error { cmd.PrintOutput(transaction.Hash()) } else { // Do a call. - err = contract.CallSubmitDepositSweepProof( - arg_sweepTx_json, - arg_sweepProof_json, - arg_mainUtxo_json, - arg_vault, + err = contract.CallSetP2TRFraudRouter( + arg_p2trFraudRouter, cmd.BlockFlagValue.Int, ) if err != nil { @@ -2324,12 +2679,12 @@ func bSubmitDepositSweepProof(c *cobra.Command, args []string) error { return nil } -func bSubmitFraudChallengeCommand() *cobra.Command { +func bSetRebateStakingCommand() *cobra.Command { c := &cobra.Command{ - Use: "submit-fraud-challenge [arg_walletPublicKey] [arg_preimageSha256] [arg_signature_json]", - Short: "Calls the payable method submitFraudChallenge on the Bridge contract.", - Args: cmd.ArgCountChecker(3), - RunE: bSubmitFraudChallenge, + 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, } @@ -2340,30 +2695,310 @@ func bSubmitFraudChallengeCommand() *cobra.Command { return c } -func bSubmitFraudChallenge(c *cobra.Command, args []string) error { +func bSetRebateStaking(c *cobra.Command, args []string) error { contract, err := initializeBridge(c) if err != nil { return err } - arg_walletPublicKey, err := hexutil.Decode(args[0]) + arg_rebateStaking, err := chainutil.AddressFromHex(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_walletPublicKey, a bytes, from passed value %v", + "couldn't parse parameter arg_rebateStaking, a address, from passed value %v", args[0], ) } - arg_preimageSha256, err := hexutil.Decode(args[1]) + + 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]", + Short: "Calls the nonpayable method setRedemptionWatchtower on the Bridge contract.", + Args: cmd.ArgCountChecker(1), + RunE: bSetRedemptionWatchtower, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSetRedemptionWatchtower(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_redemptionWatchtower, err := chainutil.AddressFromHex(args[0]) if err != nil { return fmt.Errorf( - "couldn't parse parameter arg_preimageSha256, a bytes, from passed value %v", + "couldn't parse parameter arg_redemptionWatchtower, 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.SetRedemptionWatchtower( + arg_redemptionWatchtower, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSetRedemptionWatchtower( + arg_redemptionWatchtower, + 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 bSetSpvMaintainerStatusCommand() *cobra.Command { + c := &cobra.Command{ + Use: "set-spv-maintainer-status [arg_spvMaintainer] [arg_isTrusted]", + Short: "Calls the nonpayable method setSpvMaintainerStatus on the Bridge contract.", + Args: cmd.ArgCountChecker(2), + RunE: bSetSpvMaintainerStatus, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSetSpvMaintainerStatus(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_spvMaintainer, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_spvMaintainer, a address, from passed value %v", + args[0], + ) + } + arg_isTrusted, err := strconv.ParseBool(args[1]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_isTrusted, a bool, from passed value %v", + args[1], + ) + } + + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.SetSpvMaintainerStatus( + arg_spvMaintainer, + arg_isTrusted, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSetSpvMaintainerStatus( + arg_spvMaintainer, + arg_isTrusted, + 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 bSetVaultStatusCommand() *cobra.Command { + c := &cobra.Command{ + Use: "set-vault-status [arg_vault] [arg_isTrusted]", + Short: "Calls the nonpayable method setVaultStatus on the Bridge contract.", + Args: cmd.ArgCountChecker(2), + RunE: bSetVaultStatus, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSetVaultStatus(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_vault, err := chainutil.AddressFromHex(args[0]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_vault, a address, from passed value %v", + args[0], + ) + } + arg_isTrusted, err := strconv.ParseBool(args[1]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_isTrusted, a bool, from passed value %v", args[1], ) } - arg_signature_json := abi.BitcoinTxRSVSignature{} - if err := json.Unmarshal([]byte(args[2]), &arg_signature_json); err != nil { - return fmt.Errorf("failed to unmarshal arg_signature_json to abi.BitcoinTxRSVSignature: %w", err) + var ( + transaction *types.Transaction + ) + + if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { + // Do a regular submission. Take payable into account. + transaction, err = contract.SetVaultStatus( + arg_vault, + arg_isTrusted, + ) + if err != nil { + return err + } + + cmd.PrintOutput(transaction.Hash()) + } else { + // Do a call. + err = contract.CallSetVaultStatus( + arg_vault, + arg_isTrusted, + 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 bSubmitDepositSweepProofCommand() *cobra.Command { + c := &cobra.Command{ + Use: "submit-deposit-sweep-proof [arg_sweepTx_json] [arg_sweepProof_json] [arg_mainUtxo_json] [arg_vault]", + Short: "Calls the nonpayable method submitDepositSweepProof on the Bridge contract.", + Args: cmd.ArgCountChecker(4), + RunE: bSubmitDepositSweepProof, + SilenceUsage: true, + DisableFlagsInUseLine: true, + } + + c.PreRunE = cmd.NonConstArgsChecker + cmd.InitNonConstFlags(c) + + return c +} + +func bSubmitDepositSweepProof(c *cobra.Command, args []string) error { + contract, err := initializeBridge(c) + if err != nil { + return err + } + + arg_sweepTx_json := abi.BitcoinTxInfo{} + if err := json.Unmarshal([]byte(args[0]), &arg_sweepTx_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_sweepTx_json to abi.BitcoinTxInfo: %w", err) + } + + arg_sweepProof_json := abi.BitcoinTxProof{} + if err := json.Unmarshal([]byte(args[1]), &arg_sweepProof_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_sweepProof_json to abi.BitcoinTxProof: %w", err) + } + + arg_mainUtxo_json := abi.BitcoinTxUTXO{} + if err := json.Unmarshal([]byte(args[2]), &arg_mainUtxo_json); err != nil { + return fmt.Errorf("failed to unmarshal arg_mainUtxo_json to abi.BitcoinTxUTXO: %w", err) + } + arg_vault, err := chainutil.AddressFromHex(args[3]) + if err != nil { + return fmt.Errorf( + "couldn't parse parameter arg_vault, a address, from passed value %v", + args[3], + ) } var ( @@ -2372,10 +3007,11 @@ func bSubmitFraudChallenge(c *cobra.Command, args []string) error { if shouldSubmit, _ := c.Flags().GetBool(cmd.SubmitFlag); shouldSubmit { // Do a regular submission. Take payable into account. - transaction, err = contract.SubmitFraudChallenge( - arg_walletPublicKey, - arg_preimageSha256, - arg_signature_json, + transaction, err = contract.SubmitDepositSweepProof( + arg_sweepTx_json, + arg_sweepProof_json, + arg_mainUtxo_json, + arg_vault, ) if err != nil { return err @@ -2384,10 +3020,11 @@ func bSubmitFraudChallenge(c *cobra.Command, args []string) error { cmd.PrintOutput(transaction.Hash()) } else { // Do a call. - err = contract.CallSubmitFraudChallenge( - arg_walletPublicKey, - arg_preimageSha256, - arg_signature_json, + err = contract.CallSubmitDepositSweepProof( + arg_sweepTx_json, + arg_sweepProof_json, + arg_mainUtxo_json, + arg_vault, cmd.BlockFlagValue.Int, ) if err != nil { diff --git a/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go b/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go index ae73c92607..58d179f812 100644 --- a/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go +++ b/pkg/chain/ethereum/tbtc/gen/contract/Bridge.go @@ -105,20 +105,20 @@ func NewBridge( // ----- Non-const Methods ------ // Transaction submission. -func (b *Bridge) DefeatFraudChallenge( - arg_walletPublicKey []byte, - arg_preimage []byte, - arg_witness bool, +func (b *Bridge) EcdsaWalletHeartbeatFailedCallback( + arg0 [32]byte, + arg_publicKeyX [32]byte, + arg_publicKeyY [32]byte, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction defeatFraudChallenge", + "submitting transaction ecdsaWalletHeartbeatFailedCallback", " params: ", fmt.Sprint( - arg_walletPublicKey, - arg_preimage, - arg_witness, + arg0, + arg_publicKeyX, + arg_publicKeyY, ), ) @@ -144,26 +144,26 @@ func (b *Bridge) DefeatFraudChallenge( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.DefeatFraudChallenge( + transaction, err := b.contract.EcdsaWalletHeartbeatFailedCallback( transactorOptions, - arg_walletPublicKey, - arg_preimage, - arg_witness, + arg0, + arg_publicKeyX, + arg_publicKeyY, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "defeatFraudChallenge", - arg_walletPublicKey, - arg_preimage, - arg_witness, + "ecdsaWalletHeartbeatFailedCallback", + arg0, + arg_publicKeyX, + arg_publicKeyY, ) } bLogger.Infof( - "submitted transaction defeatFraudChallenge with id: [%s] and nonce [%v]", + "submitted transaction ecdsaWalletHeartbeatFailedCallback with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -182,26 +182,26 @@ func (b *Bridge) DefeatFraudChallenge( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.DefeatFraudChallenge( + transaction, err := b.contract.EcdsaWalletHeartbeatFailedCallback( newTransactorOptions, - arg_walletPublicKey, - arg_preimage, - arg_witness, + arg0, + arg_publicKeyX, + arg_publicKeyY, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "defeatFraudChallenge", - arg_walletPublicKey, - arg_preimage, - arg_witness, + "ecdsaWalletHeartbeatFailedCallback", + arg0, + arg_publicKeyX, + arg_publicKeyY, ) } bLogger.Infof( - "submitted transaction defeatFraudChallenge with id: [%s] and nonce [%v]", + "submitted transaction ecdsaWalletHeartbeatFailedCallback with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -216,10 +216,10 @@ func (b *Bridge) DefeatFraudChallenge( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallDefeatFraudChallenge( - arg_walletPublicKey []byte, - arg_preimage []byte, - arg_witness bool, +func (b *Bridge) CallEcdsaWalletHeartbeatFailedCallback( + arg0 [32]byte, + arg_publicKeyX [32]byte, + arg_publicKeyY [32]byte, blockNumber *big.Int, ) error { var result interface{} = nil @@ -231,50 +231,48 @@ func (b *Bridge) CallDefeatFraudChallenge( b.caller, b.errorResolver, b.contractAddress, - "defeatFraudChallenge", + "ecdsaWalletHeartbeatFailedCallback", &result, - arg_walletPublicKey, - arg_preimage, - arg_witness, + arg0, + arg_publicKeyX, + arg_publicKeyY, ) return err } -func (b *Bridge) DefeatFraudChallengeGasEstimate( - arg_walletPublicKey []byte, - arg_preimage []byte, - arg_witness bool, +func (b *Bridge) EcdsaWalletHeartbeatFailedCallbackGasEstimate( + arg0 [32]byte, + arg_publicKeyX [32]byte, + arg_publicKeyY [32]byte, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "defeatFraudChallenge", + "ecdsaWalletHeartbeatFailedCallback", b.contractABI, b.transactor, - arg_walletPublicKey, - arg_preimage, - arg_witness, + arg0, + arg_publicKeyX, + arg_publicKeyY, ) return result, err } // Transaction submission. -func (b *Bridge) DefeatFraudChallengeWithHeartbeat( - arg_walletPublicKey []byte, - arg_heartbeatMessage []byte, +func (b *Bridge) FrostWalletCreatedCallback( + arg_xOnlyOutputKey [32]byte, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction defeatFraudChallengeWithHeartbeat", + "submitting transaction frostWalletCreatedCallback", " params: ", fmt.Sprint( - arg_walletPublicKey, - arg_heartbeatMessage, + arg_xOnlyOutputKey, ), ) @@ -300,24 +298,22 @@ func (b *Bridge) DefeatFraudChallengeWithHeartbeat( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.DefeatFraudChallengeWithHeartbeat( + transaction, err := b.contract.FrostWalletCreatedCallback( transactorOptions, - arg_walletPublicKey, - arg_heartbeatMessage, + arg_xOnlyOutputKey, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "defeatFraudChallengeWithHeartbeat", - arg_walletPublicKey, - arg_heartbeatMessage, + "frostWalletCreatedCallback", + arg_xOnlyOutputKey, ) } bLogger.Infof( - "submitted transaction defeatFraudChallengeWithHeartbeat with id: [%s] and nonce [%v]", + "submitted transaction frostWalletCreatedCallback with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -336,24 +332,22 @@ func (b *Bridge) DefeatFraudChallengeWithHeartbeat( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.DefeatFraudChallengeWithHeartbeat( + transaction, err := b.contract.FrostWalletCreatedCallback( newTransactorOptions, - arg_walletPublicKey, - arg_heartbeatMessage, + arg_xOnlyOutputKey, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "defeatFraudChallengeWithHeartbeat", - arg_walletPublicKey, - arg_heartbeatMessage, + "frostWalletCreatedCallback", + arg_xOnlyOutputKey, ) } bLogger.Infof( - "submitted transaction defeatFraudChallengeWithHeartbeat with id: [%s] and nonce [%v]", + "submitted transaction frostWalletCreatedCallback with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -368,9 +362,8 @@ func (b *Bridge) DefeatFraudChallengeWithHeartbeat( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallDefeatFraudChallengeWithHeartbeat( - arg_walletPublicKey []byte, - arg_heartbeatMessage []byte, +func (b *Bridge) CallFrostWalletCreatedCallback( + arg_xOnlyOutputKey [32]byte, blockNumber *big.Int, ) error { var result interface{} = nil @@ -382,49 +375,52 @@ func (b *Bridge) CallDefeatFraudChallengeWithHeartbeat( b.caller, b.errorResolver, b.contractAddress, - "defeatFraudChallengeWithHeartbeat", + "frostWalletCreatedCallback", &result, - arg_walletPublicKey, - arg_heartbeatMessage, + arg_xOnlyOutputKey, ) return err } -func (b *Bridge) DefeatFraudChallengeWithHeartbeatGasEstimate( - arg_walletPublicKey []byte, - arg_heartbeatMessage []byte, +func (b *Bridge) FrostWalletCreatedCallbackGasEstimate( + arg_xOnlyOutputKey [32]byte, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "defeatFraudChallengeWithHeartbeat", + "frostWalletCreatedCallback", b.contractABI, b.transactor, - arg_walletPublicKey, - arg_heartbeatMessage, + arg_xOnlyOutputKey, ) return result, err } // Transaction submission. -func (b *Bridge) EcdsaWalletCreatedCallback( - arg_ecdsaWalletID [32]byte, - arg_publicKeyX [32]byte, - arg_publicKeyY [32]byte, +func (b *Bridge) Initialize( + arg__bank common.Address, + arg__relay common.Address, + arg__treasury common.Address, + arg__ecdsaWalletRegistry common.Address, + arg__reimbursementPool common.Address, + arg__txProofDifficultyFactor *big.Int, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction ecdsaWalletCreatedCallback", + "submitting transaction initialize", " params: ", fmt.Sprint( - arg_ecdsaWalletID, - arg_publicKeyX, - arg_publicKeyY, + arg__bank, + arg__relay, + arg__treasury, + arg__ecdsaWalletRegistry, + arg__reimbursementPool, + arg__txProofDifficultyFactor, ), ) @@ -450,26 +446,32 @@ func (b *Bridge) EcdsaWalletCreatedCallback( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.EcdsaWalletCreatedCallback( + transaction, err := b.contract.Initialize( transactorOptions, - arg_ecdsaWalletID, - arg_publicKeyX, - arg_publicKeyY, + arg__bank, + arg__relay, + arg__treasury, + arg__ecdsaWalletRegistry, + arg__reimbursementPool, + arg__txProofDifficultyFactor, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "ecdsaWalletCreatedCallback", - arg_ecdsaWalletID, - arg_publicKeyX, - arg_publicKeyY, + "initialize", + arg__bank, + arg__relay, + arg__treasury, + arg__ecdsaWalletRegistry, + arg__reimbursementPool, + arg__txProofDifficultyFactor, ) } bLogger.Infof( - "submitted transaction ecdsaWalletCreatedCallback with id: [%s] and nonce [%v]", + "submitted transaction initialize with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -488,26 +490,32 @@ func (b *Bridge) EcdsaWalletCreatedCallback( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.EcdsaWalletCreatedCallback( + transaction, err := b.contract.Initialize( newTransactorOptions, - arg_ecdsaWalletID, - arg_publicKeyX, - arg_publicKeyY, + arg__bank, + arg__relay, + arg__treasury, + arg__ecdsaWalletRegistry, + arg__reimbursementPool, + arg__txProofDifficultyFactor, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "ecdsaWalletCreatedCallback", - arg_ecdsaWalletID, - arg_publicKeyX, - arg_publicKeyY, + "initialize", + arg__bank, + arg__relay, + arg__treasury, + arg__ecdsaWalletRegistry, + arg__reimbursementPool, + arg__txProofDifficultyFactor, ) } bLogger.Infof( - "submitted transaction ecdsaWalletCreatedCallback with id: [%s] and nonce [%v]", + "submitted transaction initialize with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -522,10 +530,13 @@ func (b *Bridge) EcdsaWalletCreatedCallback( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallEcdsaWalletCreatedCallback( - arg_ecdsaWalletID [32]byte, - arg_publicKeyX [32]byte, - arg_publicKeyY [32]byte, +func (b *Bridge) CallInitialize( + arg__bank common.Address, + arg__relay common.Address, + arg__treasury common.Address, + arg__ecdsaWalletRegistry common.Address, + arg__reimbursementPool common.Address, + arg__txProofDifficultyFactor *big.Int, blockNumber *big.Int, ) error { var result interface{} = nil @@ -537,53 +548,53 @@ func (b *Bridge) CallEcdsaWalletCreatedCallback( b.caller, b.errorResolver, b.contractAddress, - "ecdsaWalletCreatedCallback", + "initialize", &result, - arg_ecdsaWalletID, - arg_publicKeyX, - arg_publicKeyY, + arg__bank, + arg__relay, + arg__treasury, + arg__ecdsaWalletRegistry, + arg__reimbursementPool, + arg__txProofDifficultyFactor, ) return err } -func (b *Bridge) EcdsaWalletCreatedCallbackGasEstimate( - arg_ecdsaWalletID [32]byte, - arg_publicKeyX [32]byte, - arg_publicKeyY [32]byte, +func (b *Bridge) InitializeGasEstimate( + arg__bank common.Address, + arg__relay common.Address, + arg__treasury common.Address, + arg__ecdsaWalletRegistry common.Address, + arg__reimbursementPool common.Address, + arg__txProofDifficultyFactor *big.Int, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "ecdsaWalletCreatedCallback", + "initialize", b.contractABI, b.transactor, - arg_ecdsaWalletID, - arg_publicKeyX, - arg_publicKeyY, + arg__bank, + arg__relay, + arg__treasury, + arg__ecdsaWalletRegistry, + arg__reimbursementPool, + arg__txProofDifficultyFactor, ) return result, err } // Transaction submission. -func (b *Bridge) EcdsaWalletHeartbeatFailedCallback( - arg0 [32]byte, - arg_publicKeyX [32]byte, - arg_publicKeyY [32]byte, +func (b *Bridge) InitializeV2FixVaultZeroDeposit( transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction ecdsaWalletHeartbeatFailedCallback", - " params: ", - fmt.Sprint( - arg0, - arg_publicKeyX, - arg_publicKeyY, - ), + "submitting transaction initializeV2FixVaultZeroDeposit", ) b.transactionMutex.Lock() @@ -608,26 +619,20 @@ func (b *Bridge) EcdsaWalletHeartbeatFailedCallback( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.EcdsaWalletHeartbeatFailedCallback( + transaction, err := b.contract.InitializeV2FixVaultZeroDeposit( transactorOptions, - arg0, - arg_publicKeyX, - arg_publicKeyY, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "ecdsaWalletHeartbeatFailedCallback", - arg0, - arg_publicKeyX, - arg_publicKeyY, + "initializeV2FixVaultZeroDeposit", ) } bLogger.Infof( - "submitted transaction ecdsaWalletHeartbeatFailedCallback with id: [%s] and nonce [%v]", + "submitted transaction initializeV2FixVaultZeroDeposit with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -646,26 +651,20 @@ func (b *Bridge) EcdsaWalletHeartbeatFailedCallback( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.EcdsaWalletHeartbeatFailedCallback( + transaction, err := b.contract.InitializeV2FixVaultZeroDeposit( newTransactorOptions, - arg0, - arg_publicKeyX, - arg_publicKeyY, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "ecdsaWalletHeartbeatFailedCallback", - arg0, - arg_publicKeyX, - arg_publicKeyY, + "initializeV2FixVaultZeroDeposit", ) } bLogger.Infof( - "submitted transaction ecdsaWalletHeartbeatFailedCallback with id: [%s] and nonce [%v]", + "submitted transaction initializeV2FixVaultZeroDeposit with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -680,10 +679,7 @@ func (b *Bridge) EcdsaWalletHeartbeatFailedCallback( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallEcdsaWalletHeartbeatFailedCallback( - arg0 [32]byte, - arg_publicKeyX [32]byte, - arg_publicKeyY [32]byte, +func (b *Bridge) CallInitializeV2FixVaultZeroDeposit( blockNumber *big.Int, ) error { var result interface{} = nil @@ -695,58 +691,40 @@ func (b *Bridge) CallEcdsaWalletHeartbeatFailedCallback( b.caller, b.errorResolver, b.contractAddress, - "ecdsaWalletHeartbeatFailedCallback", + "initializeV2FixVaultZeroDeposit", &result, - arg0, - arg_publicKeyX, - arg_publicKeyY, ) return err } -func (b *Bridge) EcdsaWalletHeartbeatFailedCallbackGasEstimate( - arg0 [32]byte, - arg_publicKeyX [32]byte, - arg_publicKeyY [32]byte, -) (uint64, error) { +func (b *Bridge) InitializeV2FixVaultZeroDepositGasEstimate() (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "ecdsaWalletHeartbeatFailedCallback", + "initializeV2FixVaultZeroDeposit", b.contractABI, b.transactor, - arg0, - arg_publicKeyX, - arg_publicKeyY, ) return result, err } // Transaction submission. -func (b *Bridge) Initialize( - arg__bank common.Address, - arg__relay common.Address, - arg__treasury common.Address, - arg__ecdsaWalletRegistry common.Address, - arg__reimbursementPool common.Address, - arg__txProofDifficultyFactor *big.Int, +func (b *Bridge) MigrateLegacyFraudChallenges( + arg_routerKind uint8, + arg_challengeKeys []*big.Int, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction initialize", + "submitting transaction migrateLegacyFraudChallenges", " params: ", fmt.Sprint( - arg__bank, - arg__relay, - arg__treasury, - arg__ecdsaWalletRegistry, - arg__reimbursementPool, - arg__txProofDifficultyFactor, + arg_routerKind, + arg_challengeKeys, ), ) @@ -772,32 +750,24 @@ func (b *Bridge) Initialize( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.Initialize( + transaction, err := b.contract.MigrateLegacyFraudChallenges( transactorOptions, - arg__bank, - arg__relay, - arg__treasury, - arg__ecdsaWalletRegistry, - arg__reimbursementPool, - arg__txProofDifficultyFactor, + arg_routerKind, + arg_challengeKeys, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "initialize", - arg__bank, - arg__relay, - arg__treasury, - arg__ecdsaWalletRegistry, - arg__reimbursementPool, - arg__txProofDifficultyFactor, + "migrateLegacyFraudChallenges", + arg_routerKind, + arg_challengeKeys, ) } bLogger.Infof( - "submitted transaction initialize with id: [%s] and nonce [%v]", + "submitted transaction migrateLegacyFraudChallenges with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -816,32 +786,24 @@ func (b *Bridge) Initialize( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.Initialize( + transaction, err := b.contract.MigrateLegacyFraudChallenges( newTransactorOptions, - arg__bank, - arg__relay, - arg__treasury, - arg__ecdsaWalletRegistry, - arg__reimbursementPool, - arg__txProofDifficultyFactor, + arg_routerKind, + arg_challengeKeys, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "initialize", - arg__bank, - arg__relay, - arg__treasury, - arg__ecdsaWalletRegistry, - arg__reimbursementPool, - arg__txProofDifficultyFactor, + "migrateLegacyFraudChallenges", + arg_routerKind, + arg_challengeKeys, ) } bLogger.Infof( - "submitted transaction initialize with id: [%s] and nonce [%v]", + "submitted transaction migrateLegacyFraudChallenges with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -856,13 +818,9 @@ func (b *Bridge) Initialize( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallInitialize( - arg__bank common.Address, - arg__relay common.Address, - arg__treasury common.Address, - arg__ecdsaWalletRegistry common.Address, - arg__reimbursementPool common.Address, - arg__txProofDifficultyFactor *big.Int, +func (b *Bridge) CallMigrateLegacyFraudChallenges( + arg_routerKind uint8, + arg_challengeKeys []*big.Int, blockNumber *big.Int, ) error { var result interface{} = nil @@ -874,214 +832,44 @@ func (b *Bridge) CallInitialize( b.caller, b.errorResolver, b.contractAddress, - "initialize", + "migrateLegacyFraudChallenges", &result, - arg__bank, - arg__relay, - arg__treasury, - arg__ecdsaWalletRegistry, - arg__reimbursementPool, - arg__txProofDifficultyFactor, + arg_routerKind, + arg_challengeKeys, ) return err } -func (b *Bridge) InitializeGasEstimate( - arg__bank common.Address, - arg__relay common.Address, - arg__treasury common.Address, - arg__ecdsaWalletRegistry common.Address, - arg__reimbursementPool common.Address, - arg__txProofDifficultyFactor *big.Int, +func (b *Bridge) MigrateLegacyFraudChallengesGasEstimate( + arg_routerKind uint8, + arg_challengeKeys []*big.Int, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "initialize", + "migrateLegacyFraudChallenges", b.contractABI, b.transactor, - arg__bank, - arg__relay, - arg__treasury, - arg__ecdsaWalletRegistry, - arg__reimbursementPool, - arg__txProofDifficultyFactor, + arg_routerKind, + arg_challengeKeys, ) return result, err } // Transaction submission. -func (b *Bridge) NotifyFraudChallengeDefeatTimeout( - arg_walletPublicKey []byte, +func (b *Bridge) NotifyMovedFundsSweepTimeout( + arg_movingFundsTxHash [32]byte, + arg_movingFundsTxOutputIndex uint32, arg_walletMembersIDs []uint32, - arg_preimageSha256 []byte, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction notifyFraudChallengeDefeatTimeout", - " params: ", - fmt.Sprint( - arg_walletPublicKey, - arg_walletMembersIDs, - arg_preimageSha256, - ), - ) - - 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.NotifyFraudChallengeDefeatTimeout( - transactorOptions, - arg_walletPublicKey, - arg_walletMembersIDs, - arg_preimageSha256, - ) - if err != nil { - return transaction, b.errorResolver.ResolveError( - err, - b.transactorOptions.From, - nil, - "notifyFraudChallengeDefeatTimeout", - arg_walletPublicKey, - arg_walletMembersIDs, - arg_preimageSha256, - ) - } - - bLogger.Infof( - "submitted transaction notifyFraudChallengeDefeatTimeout 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.NotifyFraudChallengeDefeatTimeout( - newTransactorOptions, - arg_walletPublicKey, - arg_walletMembersIDs, - arg_preimageSha256, - ) - if err != nil { - return nil, b.errorResolver.ResolveError( - err, - b.transactorOptions.From, - nil, - "notifyFraudChallengeDefeatTimeout", - arg_walletPublicKey, - arg_walletMembersIDs, - arg_preimageSha256, - ) - } - - bLogger.Infof( - "submitted transaction notifyFraudChallengeDefeatTimeout 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) CallNotifyFraudChallengeDefeatTimeout( - arg_walletPublicKey []byte, - arg_walletMembersIDs []uint32, - arg_preimageSha256 []byte, - blockNumber *big.Int, -) error { - var result interface{} = nil - - err := chainutil.CallAtBlock( - b.transactorOptions.From, - blockNumber, nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "notifyFraudChallengeDefeatTimeout", - &result, - arg_walletPublicKey, - arg_walletMembersIDs, - arg_preimageSha256, - ) - - return err -} - -func (b *Bridge) NotifyFraudChallengeDefeatTimeoutGasEstimate( - arg_walletPublicKey []byte, - arg_walletMembersIDs []uint32, - arg_preimageSha256 []byte, -) (uint64, error) { - var result uint64 - - result, err := chainutil.EstimateGas( - b.callerOptions.From, - b.contractAddress, - "notifyFraudChallengeDefeatTimeout", - b.contractABI, - b.transactor, - arg_walletPublicKey, - arg_walletMembersIDs, - arg_preimageSha256, - ) - - return result, err -} - -// Transaction submission. -func (b *Bridge) NotifyMovedFundsSweepTimeout( - arg_movingFundsTxHash [32]byte, - arg_movingFundsTxOutputIndex uint32, - arg_walletMembersIDs []uint32, - - transactionOptions ...chainutil.TransactionOptions, -) (*types.Transaction, error) { - bLogger.Debug( - "submitting transaction notifyMovedFundsSweepTimeout", + "submitting transaction notifyMovedFundsSweepTimeout", " params: ", fmt.Sprint( arg_movingFundsTxHash, @@ -2721,19 +2509,12 @@ func (b *Bridge) ResetMovingFundsTimeoutGasEstimate( } // Transaction submission. -func (b *Bridge) RevealDeposit( - arg_fundingTx abi.BitcoinTxInfo, - arg_reveal abi.DepositDepositRevealInfo, +func (b *Bridge) RetireEcdsa( transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction revealDeposit", - " params: ", - fmt.Sprint( - arg_fundingTx, - arg_reveal, - ), + "submitting transaction retireEcdsa", ) b.transactionMutex.Lock() @@ -2758,24 +2539,20 @@ func (b *Bridge) RevealDeposit( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.RevealDeposit( + transaction, err := b.contract.RetireEcdsa( transactorOptions, - arg_fundingTx, - arg_reveal, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "revealDeposit", - arg_fundingTx, - arg_reveal, + "retireEcdsa", ) } bLogger.Infof( - "submitted transaction revealDeposit with id: [%s] and nonce [%v]", + "submitted transaction retireEcdsa with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -2794,24 +2571,20 @@ func (b *Bridge) RevealDeposit( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.RevealDeposit( + transaction, err := b.contract.RetireEcdsa( newTransactorOptions, - arg_fundingTx, - arg_reveal, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "revealDeposit", - arg_fundingTx, - arg_reveal, + "retireEcdsa", ) } bLogger.Infof( - "submitted transaction revealDeposit with id: [%s] and nonce [%v]", + "submitted transaction retireEcdsa with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -2826,9 +2599,7 @@ func (b *Bridge) RevealDeposit( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallRevealDeposit( - arg_fundingTx abi.BitcoinTxInfo, - arg_reveal abi.DepositDepositRevealInfo, +func (b *Bridge) CallRetireEcdsa( blockNumber *big.Int, ) error { var result interface{} = nil @@ -2840,49 +2611,40 @@ func (b *Bridge) CallRevealDeposit( b.caller, b.errorResolver, b.contractAddress, - "revealDeposit", + "retireEcdsa", &result, - arg_fundingTx, - arg_reveal, ) return err } -func (b *Bridge) RevealDepositGasEstimate( - arg_fundingTx abi.BitcoinTxInfo, - arg_reveal abi.DepositDepositRevealInfo, -) (uint64, error) { +func (b *Bridge) RetireEcdsaGasEstimate() (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "revealDeposit", + "retireEcdsa", b.contractABI, b.transactor, - arg_fundingTx, - arg_reveal, ) return result, err } // Transaction submission. -func (b *Bridge) RevealDepositWithExtraData( +func (b *Bridge) RevealDeposit( arg_fundingTx abi.BitcoinTxInfo, arg_reveal abi.DepositDepositRevealInfo, - arg_extraData [32]byte, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction revealDepositWithExtraData", + "submitting transaction revealDeposit", " params: ", fmt.Sprint( arg_fundingTx, arg_reveal, - arg_extraData, ), ) @@ -2908,26 +2670,24 @@ func (b *Bridge) RevealDepositWithExtraData( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.RevealDepositWithExtraData( + transaction, err := b.contract.RevealDeposit( transactorOptions, arg_fundingTx, arg_reveal, - arg_extraData, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "revealDepositWithExtraData", + "revealDeposit", arg_fundingTx, arg_reveal, - arg_extraData, ) } bLogger.Infof( - "submitted transaction revealDepositWithExtraData with id: [%s] and nonce [%v]", + "submitted transaction revealDeposit with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -2946,26 +2706,24 @@ func (b *Bridge) RevealDepositWithExtraData( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.RevealDepositWithExtraData( + transaction, err := b.contract.RevealDeposit( newTransactorOptions, arg_fundingTx, arg_reveal, - arg_extraData, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "revealDepositWithExtraData", + "revealDeposit", arg_fundingTx, arg_reveal, - arg_extraData, ) } bLogger.Infof( - "submitted transaction revealDepositWithExtraData with id: [%s] and nonce [%v]", + "submitted transaction revealDeposit with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -2980,10 +2738,9 @@ func (b *Bridge) RevealDepositWithExtraData( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallRevealDepositWithExtraData( +func (b *Bridge) CallRevealDeposit( arg_fundingTx abi.BitcoinTxInfo, arg_reveal abi.DepositDepositRevealInfo, - arg_extraData [32]byte, blockNumber *big.Int, ) error { var result interface{} = nil @@ -2995,48 +2752,49 @@ func (b *Bridge) CallRevealDepositWithExtraData( b.caller, b.errorResolver, b.contractAddress, - "revealDepositWithExtraData", + "revealDeposit", &result, arg_fundingTx, arg_reveal, - arg_extraData, ) return err } -func (b *Bridge) RevealDepositWithExtraDataGasEstimate( +func (b *Bridge) RevealDepositGasEstimate( arg_fundingTx abi.BitcoinTxInfo, arg_reveal abi.DepositDepositRevealInfo, - arg_extraData [32]byte, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "revealDepositWithExtraData", + "revealDeposit", b.contractABI, b.transactor, arg_fundingTx, arg_reveal, - arg_extraData, ) return result, err } // Transaction submission. -func (b *Bridge) SetRedemptionWatchtower( - arg_redemptionWatchtower common.Address, +func (b *Bridge) RevealDepositWithExtraData( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositDepositRevealInfo, + arg_extraData [32]byte, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction setRedemptionWatchtower", + "submitting transaction revealDepositWithExtraData", " params: ", fmt.Sprint( - arg_redemptionWatchtower, + arg_fundingTx, + arg_reveal, + arg_extraData, ), ) @@ -3062,22 +2820,26 @@ func (b *Bridge) SetRedemptionWatchtower( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SetRedemptionWatchtower( + transaction, err := b.contract.RevealDepositWithExtraData( transactorOptions, - arg_redemptionWatchtower, + arg_fundingTx, + arg_reveal, + arg_extraData, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "setRedemptionWatchtower", - arg_redemptionWatchtower, + "revealDepositWithExtraData", + arg_fundingTx, + arg_reveal, + arg_extraData, ) } bLogger.Infof( - "submitted transaction setRedemptionWatchtower with id: [%s] and nonce [%v]", + "submitted transaction revealDepositWithExtraData with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3096,22 +2858,26 @@ func (b *Bridge) SetRedemptionWatchtower( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SetRedemptionWatchtower( + transaction, err := b.contract.RevealDepositWithExtraData( newTransactorOptions, - arg_redemptionWatchtower, + arg_fundingTx, + arg_reveal, + arg_extraData, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "setRedemptionWatchtower", - arg_redemptionWatchtower, + "revealDepositWithExtraData", + arg_fundingTx, + arg_reveal, + arg_extraData, ) } bLogger.Infof( - "submitted transaction setRedemptionWatchtower with id: [%s] and nonce [%v]", + "submitted transaction revealDepositWithExtraData with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3126,8 +2892,10 @@ func (b *Bridge) SetRedemptionWatchtower( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSetRedemptionWatchtower( - arg_redemptionWatchtower common.Address, +func (b *Bridge) CallRevealDepositWithExtraData( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositDepositRevealInfo, + arg_extraData [32]byte, blockNumber *big.Int, ) error { var result interface{} = nil @@ -3139,44 +2907,50 @@ func (b *Bridge) CallSetRedemptionWatchtower( b.caller, b.errorResolver, b.contractAddress, - "setRedemptionWatchtower", + "revealDepositWithExtraData", &result, - arg_redemptionWatchtower, + arg_fundingTx, + arg_reveal, + arg_extraData, ) return err } -func (b *Bridge) SetRedemptionWatchtowerGasEstimate( - arg_redemptionWatchtower common.Address, -) (uint64, error) { - var result uint64 +func (b *Bridge) RevealDepositWithExtraDataGasEstimate( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositDepositRevealInfo, + arg_extraData [32]byte, +) (uint64, error) { + var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "setRedemptionWatchtower", + "revealDepositWithExtraData", b.contractABI, b.transactor, - arg_redemptionWatchtower, + arg_fundingTx, + arg_reveal, + arg_extraData, ) return result, err } // Transaction submission. -func (b *Bridge) SetSpvMaintainerStatus( - arg_spvMaintainer common.Address, - arg_isTrusted bool, +func (b *Bridge) RevealTaprootDeposit( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositTaprootDepositRevealInfo, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction setSpvMaintainerStatus", + "submitting transaction revealTaprootDeposit", " params: ", fmt.Sprint( - arg_spvMaintainer, - arg_isTrusted, + arg_fundingTx, + arg_reveal, ), ) @@ -3202,24 +2976,24 @@ func (b *Bridge) SetSpvMaintainerStatus( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SetSpvMaintainerStatus( + transaction, err := b.contract.RevealTaprootDeposit( transactorOptions, - arg_spvMaintainer, - arg_isTrusted, + arg_fundingTx, + arg_reveal, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "setSpvMaintainerStatus", - arg_spvMaintainer, - arg_isTrusted, + "revealTaprootDeposit", + arg_fundingTx, + arg_reveal, ) } bLogger.Infof( - "submitted transaction setSpvMaintainerStatus with id: [%s] and nonce [%v]", + "submitted transaction revealTaprootDeposit with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3238,24 +3012,24 @@ func (b *Bridge) SetSpvMaintainerStatus( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SetSpvMaintainerStatus( + transaction, err := b.contract.RevealTaprootDeposit( newTransactorOptions, - arg_spvMaintainer, - arg_isTrusted, + arg_fundingTx, + arg_reveal, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "setSpvMaintainerStatus", - arg_spvMaintainer, - arg_isTrusted, + "revealTaprootDeposit", + arg_fundingTx, + arg_reveal, ) } bLogger.Infof( - "submitted transaction setSpvMaintainerStatus with id: [%s] and nonce [%v]", + "submitted transaction revealTaprootDeposit with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3270,9 +3044,9 @@ func (b *Bridge) SetSpvMaintainerStatus( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSetSpvMaintainerStatus( - arg_spvMaintainer common.Address, - arg_isTrusted bool, +func (b *Bridge) CallRevealTaprootDeposit( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositTaprootDepositRevealInfo, blockNumber *big.Int, ) error { var result interface{} = nil @@ -3284,47 +3058,49 @@ func (b *Bridge) CallSetSpvMaintainerStatus( b.caller, b.errorResolver, b.contractAddress, - "setSpvMaintainerStatus", + "revealTaprootDeposit", &result, - arg_spvMaintainer, - arg_isTrusted, + arg_fundingTx, + arg_reveal, ) return err } -func (b *Bridge) SetSpvMaintainerStatusGasEstimate( - arg_spvMaintainer common.Address, - arg_isTrusted bool, +func (b *Bridge) RevealTaprootDepositGasEstimate( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositTaprootDepositRevealInfo, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "setSpvMaintainerStatus", + "revealTaprootDeposit", b.contractABI, b.transactor, - arg_spvMaintainer, - arg_isTrusted, + arg_fundingTx, + arg_reveal, ) return result, err } // Transaction submission. -func (b *Bridge) SetVaultStatus( - arg_vault common.Address, - arg_isTrusted bool, +func (b *Bridge) RevealTaprootDepositWithExtraData( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositTaprootDepositRevealInfo, + arg_extraData [32]byte, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction setVaultStatus", + "submitting transaction revealTaprootDepositWithExtraData", " params: ", fmt.Sprint( - arg_vault, - arg_isTrusted, + arg_fundingTx, + arg_reveal, + arg_extraData, ), ) @@ -3350,24 +3126,26 @@ func (b *Bridge) SetVaultStatus( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SetVaultStatus( + transaction, err := b.contract.RevealTaprootDepositWithExtraData( transactorOptions, - arg_vault, - arg_isTrusted, + arg_fundingTx, + arg_reveal, + arg_extraData, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "setVaultStatus", - arg_vault, - arg_isTrusted, + "revealTaprootDepositWithExtraData", + arg_fundingTx, + arg_reveal, + arg_extraData, ) } bLogger.Infof( - "submitted transaction setVaultStatus with id: [%s] and nonce [%v]", + "submitted transaction revealTaprootDepositWithExtraData with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3386,24 +3164,26 @@ func (b *Bridge) SetVaultStatus( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SetVaultStatus( + transaction, err := b.contract.RevealTaprootDepositWithExtraData( newTransactorOptions, - arg_vault, - arg_isTrusted, + arg_fundingTx, + arg_reveal, + arg_extraData, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "setVaultStatus", - arg_vault, - arg_isTrusted, + "revealTaprootDepositWithExtraData", + arg_fundingTx, + arg_reveal, + arg_extraData, ) } bLogger.Infof( - "submitted transaction setVaultStatus with id: [%s] and nonce [%v]", + "submitted transaction revealTaprootDepositWithExtraData with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3418,9 +3198,10 @@ func (b *Bridge) SetVaultStatus( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSetVaultStatus( - arg_vault common.Address, - arg_isTrusted bool, +func (b *Bridge) CallRevealTaprootDepositWithExtraData( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositTaprootDepositRevealInfo, + arg_extraData [32]byte, blockNumber *big.Int, ) error { var result interface{} = nil @@ -3432,51 +3213,48 @@ func (b *Bridge) CallSetVaultStatus( b.caller, b.errorResolver, b.contractAddress, - "setVaultStatus", + "revealTaprootDepositWithExtraData", &result, - arg_vault, - arg_isTrusted, + arg_fundingTx, + arg_reveal, + arg_extraData, ) return err } -func (b *Bridge) SetVaultStatusGasEstimate( - arg_vault common.Address, - arg_isTrusted bool, +func (b *Bridge) RevealTaprootDepositWithExtraDataGasEstimate( + arg_fundingTx abi.BitcoinTxInfo, + arg_reveal abi.DepositTaprootDepositRevealInfo, + arg_extraData [32]byte, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "setVaultStatus", + "revealTaprootDepositWithExtraData", b.contractABI, b.transactor, - arg_vault, - arg_isTrusted, + arg_fundingTx, + arg_reveal, + arg_extraData, ) return result, err } // Transaction submission. -func (b *Bridge) SubmitDepositSweepProof( - arg_sweepTx abi.BitcoinTxInfo, - arg_sweepProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_vault common.Address, +func (b *Bridge) SetEcdsaFraudRouter( + arg_ecdsaFraudRouter common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction submitDepositSweepProof", + "submitting transaction setEcdsaFraudRouter", " params: ", fmt.Sprint( - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, - arg_vault, + arg_ecdsaFraudRouter, ), ) @@ -3502,28 +3280,22 @@ func (b *Bridge) SubmitDepositSweepProof( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SubmitDepositSweepProof( + transaction, err := b.contract.SetEcdsaFraudRouter( transactorOptions, - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, - arg_vault, + arg_ecdsaFraudRouter, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitDepositSweepProof", - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, - arg_vault, + "setEcdsaFraudRouter", + arg_ecdsaFraudRouter, ) } bLogger.Infof( - "submitted transaction submitDepositSweepProof with id: [%s] and nonce [%v]", + "submitted transaction setEcdsaFraudRouter with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3542,28 +3314,22 @@ func (b *Bridge) SubmitDepositSweepProof( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SubmitDepositSweepProof( + transaction, err := b.contract.SetEcdsaFraudRouter( newTransactorOptions, - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, - arg_vault, + arg_ecdsaFraudRouter, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitDepositSweepProof", - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, - arg_vault, + "setEcdsaFraudRouter", + arg_ecdsaFraudRouter, ) } bLogger.Infof( - "submitted transaction submitDepositSweepProof with id: [%s] and nonce [%v]", + "submitted transaction setEcdsaFraudRouter with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3578,11 +3344,8 @@ func (b *Bridge) SubmitDepositSweepProof( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSubmitDepositSweepProof( - arg_sweepTx abi.BitcoinTxInfo, - arg_sweepProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_vault common.Address, +func (b *Bridge) CallSetEcdsaFraudRouter( + arg_ecdsaFraudRouter common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -3594,55 +3357,42 @@ func (b *Bridge) CallSubmitDepositSweepProof( b.caller, b.errorResolver, b.contractAddress, - "submitDepositSweepProof", + "setEcdsaFraudRouter", &result, - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, - arg_vault, + arg_ecdsaFraudRouter, ) return err } -func (b *Bridge) SubmitDepositSweepProofGasEstimate( - arg_sweepTx abi.BitcoinTxInfo, - arg_sweepProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_vault common.Address, +func (b *Bridge) SetEcdsaFraudRouterGasEstimate( + arg_ecdsaFraudRouter common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "submitDepositSweepProof", + "setEcdsaFraudRouter", b.contractABI, b.transactor, - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, - arg_vault, + arg_ecdsaFraudRouter, ) return result, err } // Transaction submission. -func (b *Bridge) SubmitFraudChallenge( - arg_walletPublicKey []byte, - arg_preimageSha256 []byte, - arg_signature abi.BitcoinTxRSVSignature, +func (b *Bridge) SetFrostWalletRegistry( + arg_frostWalletRegistry common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction submitFraudChallenge", + "submitting transaction setFrostWalletRegistry", " params: ", fmt.Sprint( - arg_walletPublicKey, - arg_preimageSha256, - arg_signature, + arg_frostWalletRegistry, ), ) @@ -3668,26 +3418,22 @@ func (b *Bridge) SubmitFraudChallenge( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SubmitFraudChallenge( + transaction, err := b.contract.SetFrostWalletRegistry( transactorOptions, - arg_walletPublicKey, - arg_preimageSha256, - arg_signature, + arg_frostWalletRegistry, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitFraudChallenge", - arg_walletPublicKey, - arg_preimageSha256, - arg_signature, + "setFrostWalletRegistry", + arg_frostWalletRegistry, ) } bLogger.Infof( - "submitted transaction submitFraudChallenge with id: [%s] and nonce [%v]", + "submitted transaction setFrostWalletRegistry with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3706,26 +3452,22 @@ func (b *Bridge) SubmitFraudChallenge( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SubmitFraudChallenge( + transaction, err := b.contract.SetFrostWalletRegistry( newTransactorOptions, - arg_walletPublicKey, - arg_preimageSha256, - arg_signature, + arg_frostWalletRegistry, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitFraudChallenge", - arg_walletPublicKey, - arg_preimageSha256, - arg_signature, + "setFrostWalletRegistry", + arg_frostWalletRegistry, ) } bLogger.Infof( - "submitted transaction submitFraudChallenge with id: [%s] and nonce [%v]", + "submitted transaction setFrostWalletRegistry with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3740,10 +3482,8 @@ func (b *Bridge) SubmitFraudChallenge( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSubmitFraudChallenge( - arg_walletPublicKey []byte, - arg_preimageSha256 []byte, - arg_signature abi.BitcoinTxRSVSignature, +func (b *Bridge) CallSetFrostWalletRegistry( + arg_frostWalletRegistry common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -3755,52 +3495,42 @@ func (b *Bridge) CallSubmitFraudChallenge( b.caller, b.errorResolver, b.contractAddress, - "submitFraudChallenge", + "setFrostWalletRegistry", &result, - arg_walletPublicKey, - arg_preimageSha256, - arg_signature, + arg_frostWalletRegistry, ) return err } -func (b *Bridge) SubmitFraudChallengeGasEstimate( - arg_walletPublicKey []byte, - arg_preimageSha256 []byte, - arg_signature abi.BitcoinTxRSVSignature, +func (b *Bridge) SetFrostWalletRegistryGasEstimate( + arg_frostWalletRegistry common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "submitFraudChallenge", + "setFrostWalletRegistry", b.contractABI, b.transactor, - arg_walletPublicKey, - arg_preimageSha256, - arg_signature, + arg_frostWalletRegistry, ) return result, err } // Transaction submission. -func (b *Bridge) SubmitMovedFundsSweepProof( - arg_sweepTx abi.BitcoinTxInfo, - arg_sweepProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, +func (b *Bridge) SetLifecycleRouter( + arg_lifecycleRouter common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction submitMovedFundsSweepProof", + "submitting transaction setLifecycleRouter", " params: ", fmt.Sprint( - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, + arg_lifecycleRouter, ), ) @@ -3826,26 +3556,22 @@ func (b *Bridge) SubmitMovedFundsSweepProof( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SubmitMovedFundsSweepProof( + transaction, err := b.contract.SetLifecycleRouter( transactorOptions, - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, + arg_lifecycleRouter, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitMovedFundsSweepProof", - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, + "setLifecycleRouter", + arg_lifecycleRouter, ) } bLogger.Infof( - "submitted transaction submitMovedFundsSweepProof with id: [%s] and nonce [%v]", + "submitted transaction setLifecycleRouter with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3864,26 +3590,22 @@ func (b *Bridge) SubmitMovedFundsSweepProof( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SubmitMovedFundsSweepProof( + transaction, err := b.contract.SetLifecycleRouter( newTransactorOptions, - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, + arg_lifecycleRouter, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitMovedFundsSweepProof", - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, + "setLifecycleRouter", + arg_lifecycleRouter, ) } bLogger.Infof( - "submitted transaction submitMovedFundsSweepProof with id: [%s] and nonce [%v]", + "submitted transaction setLifecycleRouter with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -3898,10 +3620,8 @@ func (b *Bridge) SubmitMovedFundsSweepProof( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSubmitMovedFundsSweepProof( - arg_sweepTx abi.BitcoinTxInfo, - arg_sweepProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, +func (b *Bridge) CallSetLifecycleRouter( + arg_lifecycleRouter common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -3913,56 +3633,42 @@ func (b *Bridge) CallSubmitMovedFundsSweepProof( b.caller, b.errorResolver, b.contractAddress, - "submitMovedFundsSweepProof", + "setLifecycleRouter", &result, - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, + arg_lifecycleRouter, ) return err } -func (b *Bridge) SubmitMovedFundsSweepProofGasEstimate( - arg_sweepTx abi.BitcoinTxInfo, - arg_sweepProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, +func (b *Bridge) SetLifecycleRouterGasEstimate( + arg_lifecycleRouter common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "submitMovedFundsSweepProof", + "setLifecycleRouter", b.contractABI, b.transactor, - arg_sweepTx, - arg_sweepProof, - arg_mainUtxo, + arg_lifecycleRouter, ) return result, err } // Transaction submission. -func (b *Bridge) SubmitMovingFundsCommitment( - arg_walletPubKeyHash [20]byte, - arg_walletMainUtxo abi.BitcoinTxUTXO, - arg_walletMembersIDs []uint32, - arg_walletMemberIndex *big.Int, - arg_targetWallets [][20]byte, +func (b *Bridge) SetP2TRFraudRouter( + arg_p2trFraudRouter common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction submitMovingFundsCommitment", + "submitting transaction setP2TRFraudRouter", " params: ", fmt.Sprint( - arg_walletPubKeyHash, - arg_walletMainUtxo, - arg_walletMembersIDs, - arg_walletMemberIndex, - arg_targetWallets, + arg_p2trFraudRouter, ), ) @@ -3988,30 +3694,22 @@ func (b *Bridge) SubmitMovingFundsCommitment( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SubmitMovingFundsCommitment( + transaction, err := b.contract.SetP2TRFraudRouter( transactorOptions, - arg_walletPubKeyHash, - arg_walletMainUtxo, - arg_walletMembersIDs, - arg_walletMemberIndex, - arg_targetWallets, + arg_p2trFraudRouter, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitMovingFundsCommitment", - arg_walletPubKeyHash, - arg_walletMainUtxo, - arg_walletMembersIDs, - arg_walletMemberIndex, - arg_targetWallets, + "setP2TRFraudRouter", + arg_p2trFraudRouter, ) } bLogger.Infof( - "submitted transaction submitMovingFundsCommitment with id: [%s] and nonce [%v]", + "submitted transaction setP2TRFraudRouter with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4030,30 +3728,22 @@ func (b *Bridge) SubmitMovingFundsCommitment( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SubmitMovingFundsCommitment( + transaction, err := b.contract.SetP2TRFraudRouter( newTransactorOptions, - arg_walletPubKeyHash, - arg_walletMainUtxo, - arg_walletMembersIDs, - arg_walletMemberIndex, - arg_targetWallets, + arg_p2trFraudRouter, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitMovingFundsCommitment", - arg_walletPubKeyHash, - arg_walletMainUtxo, - arg_walletMembersIDs, - arg_walletMemberIndex, - arg_targetWallets, + "setP2TRFraudRouter", + arg_p2trFraudRouter, ) } bLogger.Infof( - "submitted transaction submitMovingFundsCommitment with id: [%s] and nonce [%v]", + "submitted transaction setP2TRFraudRouter with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4068,12 +3758,8 @@ func (b *Bridge) SubmitMovingFundsCommitment( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSubmitMovingFundsCommitment( - arg_walletPubKeyHash [20]byte, - arg_walletMainUtxo abi.BitcoinTxUTXO, - arg_walletMembersIDs []uint32, - arg_walletMemberIndex *big.Int, - arg_targetWallets [][20]byte, +func (b *Bridge) CallSetP2TRFraudRouter( + arg_p2trFraudRouter common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -4085,60 +3771,42 @@ func (b *Bridge) CallSubmitMovingFundsCommitment( b.caller, b.errorResolver, b.contractAddress, - "submitMovingFundsCommitment", + "setP2TRFraudRouter", &result, - arg_walletPubKeyHash, - arg_walletMainUtxo, - arg_walletMembersIDs, - arg_walletMemberIndex, - arg_targetWallets, + arg_p2trFraudRouter, ) return err } -func (b *Bridge) SubmitMovingFundsCommitmentGasEstimate( - arg_walletPubKeyHash [20]byte, - arg_walletMainUtxo abi.BitcoinTxUTXO, - arg_walletMembersIDs []uint32, - arg_walletMemberIndex *big.Int, - arg_targetWallets [][20]byte, +func (b *Bridge) SetP2TRFraudRouterGasEstimate( + arg_p2trFraudRouter common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "submitMovingFundsCommitment", + "setP2TRFraudRouter", b.contractABI, b.transactor, - arg_walletPubKeyHash, - arg_walletMainUtxo, - arg_walletMembersIDs, - arg_walletMemberIndex, - arg_targetWallets, + arg_p2trFraudRouter, ) return result, err } // Transaction submission. -func (b *Bridge) SubmitMovingFundsProof( - arg_movingFundsTx abi.BitcoinTxInfo, - arg_movingFundsProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_walletPubKeyHash [20]byte, +func (b *Bridge) SetRebateStaking( + arg_rebateStaking common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction submitMovingFundsProof", + "submitting transaction setRebateStaking", " params: ", fmt.Sprint( - arg_movingFundsTx, - arg_movingFundsProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_rebateStaking, ), ) @@ -4164,28 +3832,22 @@ func (b *Bridge) SubmitMovingFundsProof( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SubmitMovingFundsProof( + transaction, err := b.contract.SetRebateStaking( transactorOptions, - arg_movingFundsTx, - arg_movingFundsProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_rebateStaking, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitMovingFundsProof", - arg_movingFundsTx, - arg_movingFundsProof, - arg_mainUtxo, - arg_walletPubKeyHash, + "setRebateStaking", + arg_rebateStaking, ) } bLogger.Infof( - "submitted transaction submitMovingFundsProof with id: [%s] and nonce [%v]", + "submitted transaction setRebateStaking with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4204,28 +3866,22 @@ func (b *Bridge) SubmitMovingFundsProof( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SubmitMovingFundsProof( + transaction, err := b.contract.SetRebateStaking( newTransactorOptions, - arg_movingFundsTx, - arg_movingFundsProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_rebateStaking, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitMovingFundsProof", - arg_movingFundsTx, - arg_movingFundsProof, - arg_mainUtxo, - arg_walletPubKeyHash, + "setRebateStaking", + arg_rebateStaking, ) } bLogger.Infof( - "submitted transaction submitMovingFundsProof with id: [%s] and nonce [%v]", + "submitted transaction setRebateStaking with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4240,11 +3896,8 @@ func (b *Bridge) SubmitMovingFundsProof( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSubmitMovingFundsProof( - arg_movingFundsTx abi.BitcoinTxInfo, - arg_movingFundsProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_walletPubKeyHash [20]byte, +func (b *Bridge) CallSetRebateStaking( + arg_rebateStaking common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -4256,57 +3909,42 @@ func (b *Bridge) CallSubmitMovingFundsProof( b.caller, b.errorResolver, b.contractAddress, - "submitMovingFundsProof", + "setRebateStaking", &result, - arg_movingFundsTx, - arg_movingFundsProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_rebateStaking, ) return err } -func (b *Bridge) SubmitMovingFundsProofGasEstimate( - arg_movingFundsTx abi.BitcoinTxInfo, - arg_movingFundsProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_walletPubKeyHash [20]byte, +func (b *Bridge) SetRebateStakingGasEstimate( + arg_rebateStaking common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "submitMovingFundsProof", + "setRebateStaking", b.contractABI, b.transactor, - arg_movingFundsTx, - arg_movingFundsProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_rebateStaking, ) return result, err } // Transaction submission. -func (b *Bridge) SubmitRedemptionProof( - arg_redemptionTx abi.BitcoinTxInfo, - arg_redemptionProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_walletPubKeyHash [20]byte, +func (b *Bridge) SetRedemptionWatchtower( + arg_redemptionWatchtower common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction submitRedemptionProof", + "submitting transaction setRedemptionWatchtower", " params: ", fmt.Sprint( - arg_redemptionTx, - arg_redemptionProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_redemptionWatchtower, ), ) @@ -4332,28 +3970,22 @@ func (b *Bridge) SubmitRedemptionProof( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.SubmitRedemptionProof( + transaction, err := b.contract.SetRedemptionWatchtower( transactorOptions, - arg_redemptionTx, - arg_redemptionProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_redemptionWatchtower, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitRedemptionProof", - arg_redemptionTx, - arg_redemptionProof, - arg_mainUtxo, - arg_walletPubKeyHash, + "setRedemptionWatchtower", + arg_redemptionWatchtower, ) } bLogger.Infof( - "submitted transaction submitRedemptionProof with id: [%s] and nonce [%v]", + "submitted transaction setRedemptionWatchtower with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4372,28 +4004,22 @@ func (b *Bridge) SubmitRedemptionProof( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.SubmitRedemptionProof( + transaction, err := b.contract.SetRedemptionWatchtower( newTransactorOptions, - arg_redemptionTx, - arg_redemptionProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_redemptionWatchtower, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "submitRedemptionProof", - arg_redemptionTx, - arg_redemptionProof, - arg_mainUtxo, - arg_walletPubKeyHash, + "setRedemptionWatchtower", + arg_redemptionWatchtower, ) } bLogger.Infof( - "submitted transaction submitRedemptionProof with id: [%s] and nonce [%v]", + "submitted transaction setRedemptionWatchtower with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4408,11 +4034,8 @@ func (b *Bridge) SubmitRedemptionProof( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallSubmitRedemptionProof( - arg_redemptionTx abi.BitcoinTxInfo, - arg_redemptionProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_walletPubKeyHash [20]byte, +func (b *Bridge) CallSetRedemptionWatchtower( + arg_redemptionWatchtower common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -4424,51 +4047,44 @@ func (b *Bridge) CallSubmitRedemptionProof( b.caller, b.errorResolver, b.contractAddress, - "submitRedemptionProof", + "setRedemptionWatchtower", &result, - arg_redemptionTx, - arg_redemptionProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_redemptionWatchtower, ) return err } -func (b *Bridge) SubmitRedemptionProofGasEstimate( - arg_redemptionTx abi.BitcoinTxInfo, - arg_redemptionProof abi.BitcoinTxProof, - arg_mainUtxo abi.BitcoinTxUTXO, - arg_walletPubKeyHash [20]byte, +func (b *Bridge) SetRedemptionWatchtowerGasEstimate( + arg_redemptionWatchtower common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "submitRedemptionProof", + "setRedemptionWatchtower", b.contractABI, b.transactor, - arg_redemptionTx, - arg_redemptionProof, - arg_mainUtxo, - arg_walletPubKeyHash, + arg_redemptionWatchtower, ) return result, err } // Transaction submission. -func (b *Bridge) TransferGovernance( - arg_newGovernance common.Address, +func (b *Bridge) SetSpvMaintainerStatus( + arg_spvMaintainer common.Address, + arg_isTrusted bool, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction transferGovernance", + "submitting transaction setSpvMaintainerStatus", " params: ", fmt.Sprint( - arg_newGovernance, + arg_spvMaintainer, + arg_isTrusted, ), ) @@ -4494,22 +4110,24 @@ func (b *Bridge) TransferGovernance( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.TransferGovernance( + transaction, err := b.contract.SetSpvMaintainerStatus( transactorOptions, - arg_newGovernance, + arg_spvMaintainer, + arg_isTrusted, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "transferGovernance", - arg_newGovernance, + "setSpvMaintainerStatus", + arg_spvMaintainer, + arg_isTrusted, ) } bLogger.Infof( - "submitted transaction transferGovernance with id: [%s] and nonce [%v]", + "submitted transaction setSpvMaintainerStatus with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4528,22 +4146,24 @@ func (b *Bridge) TransferGovernance( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.TransferGovernance( + transaction, err := b.contract.SetSpvMaintainerStatus( newTransactorOptions, - arg_newGovernance, + arg_spvMaintainer, + arg_isTrusted, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "transferGovernance", - arg_newGovernance, + "setSpvMaintainerStatus", + arg_spvMaintainer, + arg_isTrusted, ) } bLogger.Infof( - "submitted transaction transferGovernance with id: [%s] and nonce [%v]", + "submitted transaction setSpvMaintainerStatus with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4558,8 +4178,9 @@ func (b *Bridge) TransferGovernance( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallTransferGovernance( - arg_newGovernance common.Address, +func (b *Bridge) CallSetSpvMaintainerStatus( + arg_spvMaintainer common.Address, + arg_isTrusted bool, blockNumber *big.Int, ) error { var result interface{} = nil @@ -4571,48 +4192,47 @@ func (b *Bridge) CallTransferGovernance( b.caller, b.errorResolver, b.contractAddress, - "transferGovernance", + "setSpvMaintainerStatus", &result, - arg_newGovernance, + arg_spvMaintainer, + arg_isTrusted, ) return err } -func (b *Bridge) TransferGovernanceGasEstimate( - arg_newGovernance common.Address, +func (b *Bridge) SetSpvMaintainerStatusGasEstimate( + arg_spvMaintainer common.Address, + arg_isTrusted bool, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "transferGovernance", + "setSpvMaintainerStatus", b.contractABI, b.transactor, - arg_newGovernance, + arg_spvMaintainer, + arg_isTrusted, ) return result, err } // Transaction submission. -func (b *Bridge) UpdateDepositParameters( - arg_depositDustThreshold uint64, - arg_depositTreasuryFeeDivisor uint64, - arg_depositTxMaxFee uint64, - arg_depositRevealAheadPeriod uint32, +func (b *Bridge) SetVaultStatus( + arg_vault common.Address, + arg_isTrusted bool, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction updateDepositParameters", + "submitting transaction setVaultStatus", " params: ", fmt.Sprint( - arg_depositDustThreshold, - arg_depositTreasuryFeeDivisor, - arg_depositTxMaxFee, - arg_depositRevealAheadPeriod, + arg_vault, + arg_isTrusted, ), ) @@ -4638,28 +4258,24 @@ func (b *Bridge) UpdateDepositParameters( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.UpdateDepositParameters( + transaction, err := b.contract.SetVaultStatus( transactorOptions, - arg_depositDustThreshold, - arg_depositTreasuryFeeDivisor, - arg_depositTxMaxFee, - arg_depositRevealAheadPeriod, + arg_vault, + arg_isTrusted, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateDepositParameters", - arg_depositDustThreshold, - arg_depositTreasuryFeeDivisor, - arg_depositTxMaxFee, - arg_depositRevealAheadPeriod, + "setVaultStatus", + arg_vault, + arg_isTrusted, ) } bLogger.Infof( - "submitted transaction updateDepositParameters with id: [%s] and nonce [%v]", + "submitted transaction setVaultStatus with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4678,28 +4294,24 @@ func (b *Bridge) UpdateDepositParameters( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.UpdateDepositParameters( + transaction, err := b.contract.SetVaultStatus( newTransactorOptions, - arg_depositDustThreshold, - arg_depositTreasuryFeeDivisor, - arg_depositTxMaxFee, - arg_depositRevealAheadPeriod, + arg_vault, + arg_isTrusted, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateDepositParameters", - arg_depositDustThreshold, - arg_depositTreasuryFeeDivisor, - arg_depositTxMaxFee, - arg_depositRevealAheadPeriod, + "setVaultStatus", + arg_vault, + arg_isTrusted, ) } bLogger.Infof( - "submitted transaction updateDepositParameters with id: [%s] and nonce [%v]", + "submitted transaction setVaultStatus with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4714,11 +4326,9 @@ func (b *Bridge) UpdateDepositParameters( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallUpdateDepositParameters( - arg_depositDustThreshold uint64, - arg_depositTreasuryFeeDivisor uint64, - arg_depositTxMaxFee uint64, - arg_depositRevealAheadPeriod uint32, +func (b *Bridge) CallSetVaultStatus( + arg_vault common.Address, + arg_isTrusted bool, blockNumber *big.Int, ) error { var result interface{} = nil @@ -4730,57 +4340,49 @@ func (b *Bridge) CallUpdateDepositParameters( b.caller, b.errorResolver, b.contractAddress, - "updateDepositParameters", + "setVaultStatus", &result, - arg_depositDustThreshold, - arg_depositTreasuryFeeDivisor, - arg_depositTxMaxFee, - arg_depositRevealAheadPeriod, + arg_vault, + arg_isTrusted, ) return err } -func (b *Bridge) UpdateDepositParametersGasEstimate( - arg_depositDustThreshold uint64, - arg_depositTreasuryFeeDivisor uint64, - arg_depositTxMaxFee uint64, - arg_depositRevealAheadPeriod uint32, +func (b *Bridge) SetVaultStatusGasEstimate( + arg_vault common.Address, + arg_isTrusted bool, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "updateDepositParameters", + "setVaultStatus", b.contractABI, b.transactor, - arg_depositDustThreshold, - arg_depositTreasuryFeeDivisor, - arg_depositTxMaxFee, - arg_depositRevealAheadPeriod, + arg_vault, + arg_isTrusted, ) return result, err } // Transaction submission. -func (b *Bridge) UpdateFraudParameters( - arg_fraudChallengeDepositAmount *big.Int, - arg_fraudChallengeDefeatTimeout uint32, - arg_fraudSlashingAmount *big.Int, - arg_fraudNotifierRewardMultiplier uint32, +func (b *Bridge) SlashWalletForFraud( + arg_walletPubKeyHash [20]byte, + arg_walletMembersIDs []uint32, + arg_challenger common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction updateFraudParameters", + "submitting transaction slashWalletForFraud", " params: ", fmt.Sprint( - arg_fraudChallengeDepositAmount, - arg_fraudChallengeDefeatTimeout, - arg_fraudSlashingAmount, - arg_fraudNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ), ) @@ -4806,28 +4408,26 @@ func (b *Bridge) UpdateFraudParameters( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.UpdateFraudParameters( + transaction, err := b.contract.SlashWalletForFraud( transactorOptions, - arg_fraudChallengeDepositAmount, - arg_fraudChallengeDefeatTimeout, - arg_fraudSlashingAmount, - arg_fraudNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateFraudParameters", - arg_fraudChallengeDepositAmount, - arg_fraudChallengeDefeatTimeout, - arg_fraudSlashingAmount, - arg_fraudNotifierRewardMultiplier, + "slashWalletForFraud", + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) } bLogger.Infof( - "submitted transaction updateFraudParameters with id: [%s] and nonce [%v]", + "submitted transaction slashWalletForFraud with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4846,28 +4446,26 @@ func (b *Bridge) UpdateFraudParameters( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.UpdateFraudParameters( + transaction, err := b.contract.SlashWalletForFraud( newTransactorOptions, - arg_fraudChallengeDepositAmount, - arg_fraudChallengeDefeatTimeout, - arg_fraudSlashingAmount, - arg_fraudNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateFraudParameters", - arg_fraudChallengeDepositAmount, - arg_fraudChallengeDefeatTimeout, - arg_fraudSlashingAmount, - arg_fraudNotifierRewardMultiplier, + "slashWalletForFraud", + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) } bLogger.Infof( - "submitted transaction updateFraudParameters with id: [%s] and nonce [%v]", + "submitted transaction slashWalletForFraud with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -4882,11 +4480,10 @@ func (b *Bridge) UpdateFraudParameters( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallUpdateFraudParameters( - arg_fraudChallengeDepositAmount *big.Int, - arg_fraudChallengeDefeatTimeout uint32, - arg_fraudSlashingAmount *big.Int, - arg_fraudNotifierRewardMultiplier uint32, +func (b *Bridge) CallSlashWalletForFraud( + arg_walletPubKeyHash [20]byte, + arg_walletMembersIDs []uint32, + arg_challenger common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -4898,71 +4495,52 @@ func (b *Bridge) CallUpdateFraudParameters( b.caller, b.errorResolver, b.contractAddress, - "updateFraudParameters", + "slashWalletForFraud", &result, - arg_fraudChallengeDepositAmount, - arg_fraudChallengeDefeatTimeout, - arg_fraudSlashingAmount, - arg_fraudNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) return err } -func (b *Bridge) UpdateFraudParametersGasEstimate( - arg_fraudChallengeDepositAmount *big.Int, - arg_fraudChallengeDefeatTimeout uint32, - arg_fraudSlashingAmount *big.Int, - arg_fraudNotifierRewardMultiplier uint32, +func (b *Bridge) SlashWalletForFraudGasEstimate( + arg_walletPubKeyHash [20]byte, + arg_walletMembersIDs []uint32, + arg_challenger common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "updateFraudParameters", + "slashWalletForFraud", b.contractABI, b.transactor, - arg_fraudChallengeDepositAmount, - arg_fraudChallengeDefeatTimeout, - arg_fraudSlashingAmount, - arg_fraudNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) return result, err } // Transaction submission. -func (b *Bridge) UpdateMovingFundsParameters( - arg_movingFundsTxMaxTotalFee uint64, - arg_movingFundsDustThreshold uint64, - arg_movingFundsTimeoutResetDelay uint32, - arg_movingFundsTimeout uint32, - arg_movingFundsTimeoutSlashingAmount *big.Int, - arg_movingFundsTimeoutNotifierRewardMultiplier uint32, - arg_movingFundsCommitmentGasOffset uint16, - arg_movedFundsSweepTxMaxTotalFee uint64, - arg_movedFundsSweepTimeout uint32, - arg_movedFundsSweepTimeoutSlashingAmount *big.Int, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier uint32, +func (b *Bridge) SlashWalletForP2TRFraud( + arg_walletPubKeyHash [20]byte, + arg_walletMembersIDs []uint32, + arg_challenger common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction updateMovingFundsParameters", + "submitting transaction slashWalletForP2TRFraud", " params: ", fmt.Sprint( - arg_movingFundsTxMaxTotalFee, - arg_movingFundsDustThreshold, - arg_movingFundsTimeoutResetDelay, - arg_movingFundsTimeout, - arg_movingFundsTimeoutSlashingAmount, - arg_movingFundsTimeoutNotifierRewardMultiplier, - arg_movingFundsCommitmentGasOffset, - arg_movedFundsSweepTxMaxTotalFee, - arg_movedFundsSweepTimeout, - arg_movedFundsSweepTimeoutSlashingAmount, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ), ) @@ -4988,42 +4566,26 @@ func (b *Bridge) UpdateMovingFundsParameters( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.UpdateMovingFundsParameters( + transaction, err := b.contract.SlashWalletForP2TRFraud( transactorOptions, - arg_movingFundsTxMaxTotalFee, - arg_movingFundsDustThreshold, - arg_movingFundsTimeoutResetDelay, - arg_movingFundsTimeout, - arg_movingFundsTimeoutSlashingAmount, - arg_movingFundsTimeoutNotifierRewardMultiplier, - arg_movingFundsCommitmentGasOffset, - arg_movedFundsSweepTxMaxTotalFee, - arg_movedFundsSweepTimeout, - arg_movedFundsSweepTimeoutSlashingAmount, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateMovingFundsParameters", - arg_movingFundsTxMaxTotalFee, - arg_movingFundsDustThreshold, - arg_movingFundsTimeoutResetDelay, - arg_movingFundsTimeout, - arg_movingFundsTimeoutSlashingAmount, - arg_movingFundsTimeoutNotifierRewardMultiplier, - arg_movingFundsCommitmentGasOffset, - arg_movedFundsSweepTxMaxTotalFee, - arg_movedFundsSweepTimeout, - arg_movedFundsSweepTimeoutSlashingAmount, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + "slashWalletForP2TRFraud", + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) } bLogger.Infof( - "submitted transaction updateMovingFundsParameters with id: [%s] and nonce [%v]", + "submitted transaction slashWalletForP2TRFraud with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -5042,42 +4604,26 @@ func (b *Bridge) UpdateMovingFundsParameters( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.UpdateMovingFundsParameters( + transaction, err := b.contract.SlashWalletForP2TRFraud( newTransactorOptions, - arg_movingFundsTxMaxTotalFee, - arg_movingFundsDustThreshold, - arg_movingFundsTimeoutResetDelay, - arg_movingFundsTimeout, - arg_movingFundsTimeoutSlashingAmount, - arg_movingFundsTimeoutNotifierRewardMultiplier, - arg_movingFundsCommitmentGasOffset, - arg_movedFundsSweepTxMaxTotalFee, - arg_movedFundsSweepTimeout, - arg_movedFundsSweepTimeoutSlashingAmount, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateMovingFundsParameters", - arg_movingFundsTxMaxTotalFee, - arg_movingFundsDustThreshold, - arg_movingFundsTimeoutResetDelay, - arg_movingFundsTimeout, - arg_movingFundsTimeoutSlashingAmount, - arg_movingFundsTimeoutNotifierRewardMultiplier, - arg_movingFundsCommitmentGasOffset, - arg_movedFundsSweepTxMaxTotalFee, - arg_movedFundsSweepTimeout, - arg_movedFundsSweepTimeoutSlashingAmount, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + "slashWalletForP2TRFraud", + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) } bLogger.Infof( - "submitted transaction updateMovingFundsParameters with id: [%s] and nonce [%v]", + "submitted transaction slashWalletForP2TRFraud with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -5092,18 +4638,10 @@ func (b *Bridge) UpdateMovingFundsParameters( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallUpdateMovingFundsParameters( - arg_movingFundsTxMaxTotalFee uint64, - arg_movingFundsDustThreshold uint64, - arg_movingFundsTimeoutResetDelay uint32, - arg_movingFundsTimeout uint32, - arg_movingFundsTimeoutSlashingAmount *big.Int, - arg_movingFundsTimeoutNotifierRewardMultiplier uint32, - arg_movingFundsCommitmentGasOffset uint16, - arg_movedFundsSweepTxMaxTotalFee uint64, - arg_movedFundsSweepTimeout uint32, - arg_movedFundsSweepTimeoutSlashingAmount *big.Int, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier uint32, +func (b *Bridge) CallSlashWalletForP2TRFraud( + arg_walletPubKeyHash [20]byte, + arg_walletMembersIDs []uint32, + arg_challenger common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -5115,84 +4653,54 @@ func (b *Bridge) CallUpdateMovingFundsParameters( b.caller, b.errorResolver, b.contractAddress, - "updateMovingFundsParameters", + "slashWalletForP2TRFraud", &result, - arg_movingFundsTxMaxTotalFee, - arg_movingFundsDustThreshold, - arg_movingFundsTimeoutResetDelay, - arg_movingFundsTimeout, - arg_movingFundsTimeoutSlashingAmount, - arg_movingFundsTimeoutNotifierRewardMultiplier, - arg_movingFundsCommitmentGasOffset, - arg_movedFundsSweepTxMaxTotalFee, - arg_movedFundsSweepTimeout, - arg_movedFundsSweepTimeoutSlashingAmount, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) return err } -func (b *Bridge) UpdateMovingFundsParametersGasEstimate( - arg_movingFundsTxMaxTotalFee uint64, - arg_movingFundsDustThreshold uint64, - arg_movingFundsTimeoutResetDelay uint32, - arg_movingFundsTimeout uint32, - arg_movingFundsTimeoutSlashingAmount *big.Int, - arg_movingFundsTimeoutNotifierRewardMultiplier uint32, - arg_movingFundsCommitmentGasOffset uint16, - arg_movedFundsSweepTxMaxTotalFee uint64, - arg_movedFundsSweepTimeout uint32, - arg_movedFundsSweepTimeoutSlashingAmount *big.Int, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier uint32, +func (b *Bridge) SlashWalletForP2TRFraudGasEstimate( + arg_walletPubKeyHash [20]byte, + arg_walletMembersIDs []uint32, + arg_challenger common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "updateMovingFundsParameters", + "slashWalletForP2TRFraud", b.contractABI, b.transactor, - arg_movingFundsTxMaxTotalFee, - arg_movingFundsDustThreshold, - arg_movingFundsTimeoutResetDelay, - arg_movingFundsTimeout, - arg_movingFundsTimeoutSlashingAmount, - arg_movingFundsTimeoutNotifierRewardMultiplier, - arg_movingFundsCommitmentGasOffset, - arg_movedFundsSweepTxMaxTotalFee, - arg_movedFundsSweepTimeout, - arg_movedFundsSweepTimeoutSlashingAmount, - arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + arg_walletPubKeyHash, + arg_walletMembersIDs, + arg_challenger, ) return result, err } // Transaction submission. -func (b *Bridge) UpdateRedemptionParameters( - arg_redemptionDustThreshold uint64, - arg_redemptionTreasuryFeeDivisor uint64, - arg_redemptionTxMaxFee uint64, - arg_redemptionTxMaxTotalFee uint64, - arg_redemptionTimeout uint32, - arg_redemptionTimeoutSlashingAmount *big.Int, - arg_redemptionTimeoutNotifierRewardMultiplier uint32, +func (b *Bridge) SubmitDepositSweepProof( + arg_sweepTx abi.BitcoinTxInfo, + arg_sweepProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_vault common.Address, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction updateRedemptionParameters", + "submitting transaction submitDepositSweepProof", " params: ", fmt.Sprint( - arg_redemptionDustThreshold, - arg_redemptionTreasuryFeeDivisor, - arg_redemptionTxMaxFee, - arg_redemptionTxMaxTotalFee, - arg_redemptionTimeout, - arg_redemptionTimeoutSlashingAmount, - arg_redemptionTimeoutNotifierRewardMultiplier, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, + arg_vault, ), ) @@ -5218,34 +4726,28 @@ func (b *Bridge) UpdateRedemptionParameters( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.UpdateRedemptionParameters( + transaction, err := b.contract.SubmitDepositSweepProof( transactorOptions, - arg_redemptionDustThreshold, - arg_redemptionTreasuryFeeDivisor, - arg_redemptionTxMaxFee, - arg_redemptionTxMaxTotalFee, - arg_redemptionTimeout, - arg_redemptionTimeoutSlashingAmount, - arg_redemptionTimeoutNotifierRewardMultiplier, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, + arg_vault, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateRedemptionParameters", - arg_redemptionDustThreshold, - arg_redemptionTreasuryFeeDivisor, - arg_redemptionTxMaxFee, - arg_redemptionTxMaxTotalFee, - arg_redemptionTimeout, - arg_redemptionTimeoutSlashingAmount, - arg_redemptionTimeoutNotifierRewardMultiplier, + "submitDepositSweepProof", + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, + arg_vault, ) } bLogger.Infof( - "submitted transaction updateRedemptionParameters with id: [%s] and nonce [%v]", + "submitted transaction submitDepositSweepProof with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -5264,34 +4766,28 @@ func (b *Bridge) UpdateRedemptionParameters( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.UpdateRedemptionParameters( + transaction, err := b.contract.SubmitDepositSweepProof( newTransactorOptions, - arg_redemptionDustThreshold, - arg_redemptionTreasuryFeeDivisor, - arg_redemptionTxMaxFee, - arg_redemptionTxMaxTotalFee, - arg_redemptionTimeout, - arg_redemptionTimeoutSlashingAmount, - arg_redemptionTimeoutNotifierRewardMultiplier, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, + arg_vault, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateRedemptionParameters", - arg_redemptionDustThreshold, - arg_redemptionTreasuryFeeDivisor, - arg_redemptionTxMaxFee, - arg_redemptionTxMaxTotalFee, - arg_redemptionTimeout, - arg_redemptionTimeoutSlashingAmount, - arg_redemptionTimeoutNotifierRewardMultiplier, + "submitDepositSweepProof", + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, + arg_vault, ) } bLogger.Infof( - "submitted transaction updateRedemptionParameters with id: [%s] and nonce [%v]", + "submitted transaction submitDepositSweepProof with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -5306,14 +4802,11 @@ func (b *Bridge) UpdateRedemptionParameters( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallUpdateRedemptionParameters( - arg_redemptionDustThreshold uint64, - arg_redemptionTreasuryFeeDivisor uint64, - arg_redemptionTxMaxFee uint64, - arg_redemptionTxMaxTotalFee uint64, - arg_redemptionTimeout uint32, - arg_redemptionTimeoutSlashingAmount *big.Int, - arg_redemptionTimeoutNotifierRewardMultiplier uint32, +func (b *Bridge) CallSubmitDepositSweepProof( + arg_sweepTx abi.BitcoinTxInfo, + arg_sweepProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_vault common.Address, blockNumber *big.Int, ) error { var result interface{} = nil @@ -5325,60 +4818,55 @@ func (b *Bridge) CallUpdateRedemptionParameters( b.caller, b.errorResolver, b.contractAddress, - "updateRedemptionParameters", + "submitDepositSweepProof", &result, - arg_redemptionDustThreshold, - arg_redemptionTreasuryFeeDivisor, - arg_redemptionTxMaxFee, - arg_redemptionTxMaxTotalFee, - arg_redemptionTimeout, - arg_redemptionTimeoutSlashingAmount, - arg_redemptionTimeoutNotifierRewardMultiplier, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, + arg_vault, ) return err } -func (b *Bridge) UpdateRedemptionParametersGasEstimate( - arg_redemptionDustThreshold uint64, - arg_redemptionTreasuryFeeDivisor uint64, - arg_redemptionTxMaxFee uint64, - arg_redemptionTxMaxTotalFee uint64, - arg_redemptionTimeout uint32, - arg_redemptionTimeoutSlashingAmount *big.Int, - arg_redemptionTimeoutNotifierRewardMultiplier uint32, +func (b *Bridge) SubmitDepositSweepProofGasEstimate( + arg_sweepTx abi.BitcoinTxInfo, + arg_sweepProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_vault common.Address, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "updateRedemptionParameters", + "submitDepositSweepProof", b.contractABI, b.transactor, - arg_redemptionDustThreshold, - arg_redemptionTreasuryFeeDivisor, - arg_redemptionTxMaxFee, - arg_redemptionTxMaxTotalFee, - arg_redemptionTimeout, - arg_redemptionTimeoutSlashingAmount, - arg_redemptionTimeoutNotifierRewardMultiplier, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, + arg_vault, ) return result, err } // Transaction submission. -func (b *Bridge) UpdateTreasury( - arg_treasury common.Address, +func (b *Bridge) SubmitMovedFundsSweepProof( + arg_sweepTx abi.BitcoinTxInfo, + arg_sweepProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction updateTreasury", + "submitting transaction submitMovedFundsSweepProof", " params: ", fmt.Sprint( - arg_treasury, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, ), ) @@ -5404,22 +4892,26 @@ func (b *Bridge) UpdateTreasury( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.UpdateTreasury( + transaction, err := b.contract.SubmitMovedFundsSweepProof( transactorOptions, - arg_treasury, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateTreasury", - arg_treasury, + "submitMovedFundsSweepProof", + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, ) } bLogger.Infof( - "submitted transaction updateTreasury with id: [%s] and nonce [%v]", + "submitted transaction submitMovedFundsSweepProof with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -5438,22 +4930,26 @@ func (b *Bridge) UpdateTreasury( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.UpdateTreasury( + transaction, err := b.contract.SubmitMovedFundsSweepProof( newTransactorOptions, - arg_treasury, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateTreasury", - arg_treasury, + "submitMovedFundsSweepProof", + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, ) } bLogger.Infof( - "submitted transaction updateTreasury with id: [%s] and nonce [%v]", + "submitted transaction submitMovedFundsSweepProof with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -5468,8 +4964,10 @@ func (b *Bridge) UpdateTreasury( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallUpdateTreasury( - arg_treasury common.Address, +func (b *Bridge) CallSubmitMovedFundsSweepProof( + arg_sweepTx abi.BitcoinTxInfo, + arg_sweepProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, blockNumber *big.Int, ) error { var result interface{} = nil @@ -5481,54 +4979,56 @@ func (b *Bridge) CallUpdateTreasury( b.caller, b.errorResolver, b.contractAddress, - "updateTreasury", + "submitMovedFundsSweepProof", &result, - arg_treasury, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, ) return err } -func (b *Bridge) UpdateTreasuryGasEstimate( - arg_treasury common.Address, +func (b *Bridge) SubmitMovedFundsSweepProofGasEstimate( + arg_sweepTx abi.BitcoinTxInfo, + arg_sweepProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "updateTreasury", + "submitMovedFundsSweepProof", b.contractABI, b.transactor, - arg_treasury, + arg_sweepTx, + arg_sweepProof, + arg_mainUtxo, ) return result, err } // Transaction submission. -func (b *Bridge) UpdateWalletParameters( - arg_walletCreationPeriod uint32, - arg_walletCreationMinBtcBalance uint64, - arg_walletCreationMaxBtcBalance uint64, - arg_walletClosureMinBtcBalance uint64, - arg_walletMaxAge uint32, - arg_walletMaxBtcTransfer uint64, - arg_walletClosingPeriod uint32, +func (b *Bridge) SubmitMovingFundsCommitment( + arg_walletPubKeyHash [20]byte, + arg_walletMainUtxo abi.BitcoinTxUTXO, + arg_walletMembersIDs []uint32, + arg_walletMemberIndex *big.Int, + arg_targetWallets [][20]byte, transactionOptions ...chainutil.TransactionOptions, ) (*types.Transaction, error) { bLogger.Debug( - "submitting transaction updateWalletParameters", + "submitting transaction submitMovingFundsCommitment", " params: ", fmt.Sprint( - arg_walletCreationPeriod, - arg_walletCreationMinBtcBalance, - arg_walletCreationMaxBtcBalance, - arg_walletClosureMinBtcBalance, - arg_walletMaxAge, - arg_walletMaxBtcTransfer, - arg_walletClosingPeriod, + arg_walletPubKeyHash, + arg_walletMainUtxo, + arg_walletMembersIDs, + arg_walletMemberIndex, + arg_targetWallets, ), ) @@ -5554,34 +5054,30 @@ func (b *Bridge) UpdateWalletParameters( transactorOptions.Nonce = new(big.Int).SetUint64(nonce) - transaction, err := b.contract.UpdateWalletParameters( + transaction, err := b.contract.SubmitMovingFundsCommitment( transactorOptions, - arg_walletCreationPeriod, - arg_walletCreationMinBtcBalance, - arg_walletCreationMaxBtcBalance, - arg_walletClosureMinBtcBalance, - arg_walletMaxAge, - arg_walletMaxBtcTransfer, - arg_walletClosingPeriod, + arg_walletPubKeyHash, + arg_walletMainUtxo, + arg_walletMembersIDs, + arg_walletMemberIndex, + arg_targetWallets, ) if err != nil { return transaction, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateWalletParameters", - arg_walletCreationPeriod, - arg_walletCreationMinBtcBalance, - arg_walletCreationMaxBtcBalance, - arg_walletClosureMinBtcBalance, - arg_walletMaxAge, - arg_walletMaxBtcTransfer, - arg_walletClosingPeriod, + "submitMovingFundsCommitment", + arg_walletPubKeyHash, + arg_walletMainUtxo, + arg_walletMembersIDs, + arg_walletMemberIndex, + arg_targetWallets, ) } bLogger.Infof( - "submitted transaction updateWalletParameters with id: [%s] and nonce [%v]", + "submitted transaction submitMovingFundsCommitment with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -5600,34 +5096,30 @@ func (b *Bridge) UpdateWalletParameters( newTransactorOptions.GasLimit = transactorOptions.GasLimit } - transaction, err := b.contract.UpdateWalletParameters( + transaction, err := b.contract.SubmitMovingFundsCommitment( newTransactorOptions, - arg_walletCreationPeriod, - arg_walletCreationMinBtcBalance, - arg_walletCreationMaxBtcBalance, - arg_walletClosureMinBtcBalance, - arg_walletMaxAge, - arg_walletMaxBtcTransfer, - arg_walletClosingPeriod, + arg_walletPubKeyHash, + arg_walletMainUtxo, + arg_walletMembersIDs, + arg_walletMemberIndex, + arg_targetWallets, ) if err != nil { return nil, b.errorResolver.ResolveError( err, b.transactorOptions.From, nil, - "updateWalletParameters", - arg_walletCreationPeriod, - arg_walletCreationMinBtcBalance, - arg_walletCreationMaxBtcBalance, - arg_walletClosureMinBtcBalance, - arg_walletMaxAge, - arg_walletMaxBtcTransfer, - arg_walletClosingPeriod, + "submitMovingFundsCommitment", + arg_walletPubKeyHash, + arg_walletMainUtxo, + arg_walletMembersIDs, + arg_walletMemberIndex, + arg_targetWallets, ) } bLogger.Infof( - "submitted transaction updateWalletParameters with id: [%s] and nonce [%v]", + "submitted transaction submitMovingFundsCommitment with id: [%s] and nonce [%v]", transaction.Hash(), transaction.Nonce(), ) @@ -5642,14 +5134,12 @@ func (b *Bridge) UpdateWalletParameters( } // Non-mutating call, not a transaction submission. -func (b *Bridge) CallUpdateWalletParameters( - arg_walletCreationPeriod uint32, - arg_walletCreationMinBtcBalance uint64, - arg_walletCreationMaxBtcBalance uint64, - arg_walletClosureMinBtcBalance uint64, - arg_walletMaxAge uint32, - arg_walletMaxBtcTransfer uint64, - arg_walletClosingPeriod uint32, +func (b *Bridge) CallSubmitMovingFundsCommitment( + arg_walletPubKeyHash [20]byte, + arg_walletMainUtxo abi.BitcoinTxUTXO, + arg_walletMembersIDs []uint32, + arg_walletMemberIndex *big.Int, + arg_targetWallets [][20]byte, blockNumber *big.Int, ) error { var result interface{} = nil @@ -5661,899 +5151,4283 @@ func (b *Bridge) CallUpdateWalletParameters( b.caller, b.errorResolver, b.contractAddress, - "updateWalletParameters", + "submitMovingFundsCommitment", &result, - arg_walletCreationPeriod, - arg_walletCreationMinBtcBalance, - arg_walletCreationMaxBtcBalance, - arg_walletClosureMinBtcBalance, - arg_walletMaxAge, - arg_walletMaxBtcTransfer, - arg_walletClosingPeriod, + arg_walletPubKeyHash, + arg_walletMainUtxo, + arg_walletMembersIDs, + arg_walletMemberIndex, + arg_targetWallets, ) return err } -func (b *Bridge) UpdateWalletParametersGasEstimate( - arg_walletCreationPeriod uint32, - arg_walletCreationMinBtcBalance uint64, - arg_walletCreationMaxBtcBalance uint64, - arg_walletClosureMinBtcBalance uint64, - arg_walletMaxAge uint32, - arg_walletMaxBtcTransfer uint64, - arg_walletClosingPeriod uint32, +func (b *Bridge) SubmitMovingFundsCommitmentGasEstimate( + arg_walletPubKeyHash [20]byte, + arg_walletMainUtxo abi.BitcoinTxUTXO, + arg_walletMembersIDs []uint32, + arg_walletMemberIndex *big.Int, + arg_targetWallets [][20]byte, ) (uint64, error) { var result uint64 result, err := chainutil.EstimateGas( b.callerOptions.From, b.contractAddress, - "updateWalletParameters", + "submitMovingFundsCommitment", b.contractABI, b.transactor, - arg_walletCreationPeriod, - arg_walletCreationMinBtcBalance, - arg_walletCreationMaxBtcBalance, - arg_walletClosureMinBtcBalance, - arg_walletMaxAge, - arg_walletMaxBtcTransfer, - arg_walletClosingPeriod, + arg_walletPubKeyHash, + arg_walletMainUtxo, + arg_walletMembersIDs, + arg_walletMemberIndex, + arg_targetWallets, ) return result, err } -// ----- Const Methods ------ +// Transaction submission. +func (b *Bridge) SubmitMovingFundsProof( + arg_movingFundsTx abi.BitcoinTxInfo, + arg_movingFundsProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_walletPubKeyHash [20]byte, -func (b *Bridge) ActiveWalletPubKeyHash() ([20]byte, error) { - result, err := b.contract.ActiveWalletPubKeyHash( - b.callerOptions, + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction submitMovingFundsProof", + " params: ", + fmt.Sprint( + arg_movingFundsTx, + arg_movingFundsProof, + arg_mainUtxo, + arg_walletPubKeyHash, + ), ) + 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 result, b.errorResolver.ResolveError( + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } + + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) + + transaction, err := b.contract.SubmitMovingFundsProof( + transactorOptions, + arg_movingFundsTx, + arg_movingFundsProof, + arg_mainUtxo, + arg_walletPubKeyHash, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( err, - b.callerOptions.From, + b.transactorOptions.From, nil, - "activeWalletPubKeyHash", + "submitMovingFundsProof", + arg_movingFundsTx, + arg_movingFundsProof, + arg_mainUtxo, + arg_walletPubKeyHash, ) } - return result, err -} - -func (b *Bridge) ActiveWalletPubKeyHashAtBlock( - 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, - "activeWalletPubKeyHash", - &result, + bLogger.Infof( + "submitted transaction submitMovingFundsProof with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), ) - return result, err -} + 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 + } -type contractReferences struct { - Bank common.Address - Relay common.Address - EcdsaWalletRegistry common.Address - ReimbursementPool common.Address -} + transaction, err := b.contract.SubmitMovingFundsProof( + newTransactorOptions, + arg_movingFundsTx, + arg_movingFundsProof, + arg_mainUtxo, + arg_walletPubKeyHash, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "submitMovingFundsProof", + arg_movingFundsTx, + arg_movingFundsProof, + arg_mainUtxo, + arg_walletPubKeyHash, + ) + } -func (b *Bridge) ContractReferences() (contractReferences, error) { - result, err := b.contract.ContractReferences( - b.callerOptions, + bLogger.Infof( + "submitted transaction submitMovingFundsProof with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, ) - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "contractReferences", - ) - } + b.nonceManager.IncrementNonce() - return result, err + return transaction, err } -func (b *Bridge) ContractReferencesAtBlock( +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallSubmitMovingFundsProof( + arg_movingFundsTx abi.BitcoinTxInfo, + arg_movingFundsProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_walletPubKeyHash [20]byte, blockNumber *big.Int, -) (contractReferences, error) { - var result contractReferences +) error { + var result interface{} = nil err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, + b.transactorOptions.From, + blockNumber, nil, b.contractABI, b.caller, b.errorResolver, b.contractAddress, - "contractReferences", + "submitMovingFundsProof", &result, + arg_movingFundsTx, + arg_movingFundsProof, + arg_mainUtxo, + arg_walletPubKeyHash, ) - return result, err + return err } -type depositParameters struct { - DepositDustThreshold uint64 - DepositTreasuryFeeDivisor uint64 - DepositTxMaxFee uint64 - DepositRevealAheadPeriod uint32 -} +func (b *Bridge) SubmitMovingFundsProofGasEstimate( + arg_movingFundsTx abi.BitcoinTxInfo, + arg_movingFundsProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_walletPubKeyHash [20]byte, +) (uint64, error) { + var result uint64 -func (b *Bridge) DepositParameters() (depositParameters, error) { - result, err := b.contract.DepositParameters( - b.callerOptions, + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "submitMovingFundsProof", + b.contractABI, + b.transactor, + arg_movingFundsTx, + arg_movingFundsProof, + arg_mainUtxo, + arg_walletPubKeyHash, ) - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "depositParameters", - ) - } - return result, err } -func (b *Bridge) DepositParametersAtBlock( - blockNumber *big.Int, -) (depositParameters, error) { - var result depositParameters +// Transaction submission. +func (b *Bridge) SubmitRedemptionProof( + arg_redemptionTx abi.BitcoinTxInfo, + arg_redemptionProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_walletPubKeyHash [20]byte, - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "depositParameters", - &result, + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction submitRedemptionProof", + " params: ", + fmt.Sprint( + arg_redemptionTx, + arg_redemptionProof, + arg_mainUtxo, + arg_walletPubKeyHash, + ), ) - return result, err -} + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() -func (b *Bridge) Deposits( - arg_depositKey *big.Int, -) (abi.DepositDepositRequest, error) { - result, err := b.contract.Deposits( - b.callerOptions, - arg_depositKey, - ) + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "deposits", - arg_depositKey, + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) } - return result, err -} - -func (b *Bridge) DepositsAtBlock( - arg_depositKey *big.Int, - blockNumber *big.Int, -) (abi.DepositDepositRequest, error) { - var result abi.DepositDepositRequest - - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "deposits", - &result, - arg_depositKey, - ) + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } - return result, err -} + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) -func (b *Bridge) FraudChallenges( - arg_challengeKey *big.Int, -) (abi.FraudFraudChallenge, error) { - result, err := b.contract.FraudChallenges( - b.callerOptions, - arg_challengeKey, + transaction, err := b.contract.SubmitRedemptionProof( + transactorOptions, + arg_redemptionTx, + arg_redemptionProof, + arg_mainUtxo, + arg_walletPubKeyHash, ) - if err != nil { - return result, b.errorResolver.ResolveError( + return transaction, b.errorResolver.ResolveError( err, - b.callerOptions.From, + b.transactorOptions.From, nil, - "fraudChallenges", - arg_challengeKey, + "submitRedemptionProof", + arg_redemptionTx, + arg_redemptionProof, + arg_mainUtxo, + arg_walletPubKeyHash, ) } - return result, err -} - -func (b *Bridge) FraudChallengesAtBlock( - arg_challengeKey *big.Int, - blockNumber *big.Int, -) (abi.FraudFraudChallenge, error) { - var result abi.FraudFraudChallenge - - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "fraudChallenges", - &result, - arg_challengeKey, + bLogger.Infof( + "submitted transaction submitRedemptionProof with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), ) - return result, err -} + 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 + } -type fraudParameters struct { - FraudChallengeDepositAmount *big.Int - FraudChallengeDefeatTimeout uint32 - FraudSlashingAmount *big.Int - FraudNotifierRewardMultiplier uint32 -} + transaction, err := b.contract.SubmitRedemptionProof( + newTransactorOptions, + arg_redemptionTx, + arg_redemptionProof, + arg_mainUtxo, + arg_walletPubKeyHash, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "submitRedemptionProof", + arg_redemptionTx, + arg_redemptionProof, + arg_mainUtxo, + arg_walletPubKeyHash, + ) + } -func (b *Bridge) FraudParameters() (fraudParameters, error) { - result, err := b.contract.FraudParameters( - b.callerOptions, + bLogger.Infof( + "submitted transaction submitRedemptionProof with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) + + return transaction, nil + }, ) - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "fraudParameters", - ) - } + b.nonceManager.IncrementNonce() - return result, err + return transaction, err } -func (b *Bridge) FraudParametersAtBlock( +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallSubmitRedemptionProof( + arg_redemptionTx abi.BitcoinTxInfo, + arg_redemptionProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_walletPubKeyHash [20]byte, blockNumber *big.Int, -) (fraudParameters, error) { - var result fraudParameters +) error { + var result interface{} = nil err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, + b.transactorOptions.From, + blockNumber, nil, b.contractABI, b.caller, b.errorResolver, b.contractAddress, - "fraudParameters", + "submitRedemptionProof", &result, + arg_redemptionTx, + arg_redemptionProof, + arg_mainUtxo, + arg_walletPubKeyHash, ) - return result, err + return err } -func (b *Bridge) GetRedemptionWatchtower() (common.Address, error) { - result, err := b.contract.GetRedemptionWatchtower( - b.callerOptions, - ) +func (b *Bridge) SubmitRedemptionProofGasEstimate( + arg_redemptionTx abi.BitcoinTxInfo, + arg_redemptionProof abi.BitcoinTxProof, + arg_mainUtxo abi.BitcoinTxUTXO, + arg_walletPubKeyHash [20]byte, +) (uint64, error) { + var result uint64 - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "getRedemptionWatchtower", - ) - } + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "submitRedemptionProof", + b.contractABI, + b.transactor, + arg_redemptionTx, + arg_redemptionProof, + arg_mainUtxo, + arg_walletPubKeyHash, + ) return result, err } -func (b *Bridge) GetRedemptionWatchtowerAtBlock( - blockNumber *big.Int, -) (common.Address, error) { - var result common.Address +// Transaction submission. +func (b *Bridge) TransferGovernance( + arg_newGovernance common.Address, - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "getRedemptionWatchtower", - &result, + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction transferGovernance", + " params: ", + fmt.Sprint( + arg_newGovernance, + ), ) - return result, err -} + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() -func (b *Bridge) Governance() (common.Address, error) { - result, err := b.contract.Governance( - b.callerOptions, - ) + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "governance", + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) } - return result, err -} - -func (b *Bridge) GovernanceAtBlock( - 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, - "governance", - &result, - ) + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } - return result, err -} + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) -func (b *Bridge) IsVaultTrusted( - arg_vault common.Address, -) (bool, error) { - result, err := b.contract.IsVaultTrusted( - b.callerOptions, - arg_vault, + transaction, err := b.contract.TransferGovernance( + transactorOptions, + arg_newGovernance, ) - if err != nil { - return result, b.errorResolver.ResolveError( + return transaction, b.errorResolver.ResolveError( err, - b.callerOptions.From, + b.transactorOptions.From, nil, - "isVaultTrusted", - arg_vault, + "transferGovernance", + arg_newGovernance, ) } - return result, err -} + bLogger.Infof( + "submitted transaction transferGovernance with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) -func (b *Bridge) IsVaultTrustedAtBlock( - arg_vault common.Address, - blockNumber *big.Int, -) (bool, error) { - var result bool + 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 + } - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "isVaultTrusted", - &result, - arg_vault, - ) + transaction, err := b.contract.TransferGovernance( + newTransactorOptions, + arg_newGovernance, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "transferGovernance", + arg_newGovernance, + ) + } - return result, err -} + bLogger.Infof( + "submitted transaction transferGovernance with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) -func (b *Bridge) LiveWalletsCount() (uint32, error) { - result, err := b.contract.LiveWalletsCount( - b.callerOptions, + return transaction, nil + }, ) - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "liveWalletsCount", - ) - } + b.nonceManager.IncrementNonce() - return result, err + return transaction, err } -func (b *Bridge) LiveWalletsCountAtBlock( +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallTransferGovernance( + arg_newGovernance common.Address, blockNumber *big.Int, -) (uint32, error) { - var result uint32 +) error { + var result interface{} = nil err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, + b.transactorOptions.From, + blockNumber, nil, b.contractABI, b.caller, b.errorResolver, b.contractAddress, - "liveWalletsCount", + "transferGovernance", &result, + arg_newGovernance, ) - return result, err -} - -func (b *Bridge) MovedFundsSweepRequests( - arg_requestKey *big.Int, -) (abi.MovingFundsMovedFundsSweepRequest, error) { - result, err := b.contract.MovedFundsSweepRequests( - b.callerOptions, - arg_requestKey, - ) - - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "movedFundsSweepRequests", - arg_requestKey, - ) - } - - return result, err + return err } -func (b *Bridge) MovedFundsSweepRequestsAtBlock( - arg_requestKey *big.Int, - blockNumber *big.Int, -) (abi.MovingFundsMovedFundsSweepRequest, error) { - var result abi.MovingFundsMovedFundsSweepRequest +func (b *Bridge) TransferGovernanceGasEstimate( + arg_newGovernance common.Address, +) (uint64, error) { + var result uint64 - err := chainutil.CallAtBlock( + result, err := chainutil.EstimateGas( b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, b.contractAddress, - "movedFundsSweepRequests", - &result, - arg_requestKey, + "transferGovernance", + b.contractABI, + b.transactor, + arg_newGovernance, ) return result, err } -type movingFundsParameters struct { - MovingFundsTxMaxTotalFee uint64 - MovingFundsDustThreshold uint64 - MovingFundsTimeoutResetDelay uint32 - MovingFundsTimeout uint32 - MovingFundsTimeoutSlashingAmount *big.Int - MovingFundsTimeoutNotifierRewardMultiplier uint32 - MovingFundsCommitmentGasOffset uint16 - MovedFundsSweepTxMaxTotalFee uint64 - MovedFundsSweepTimeout uint32 - MovedFundsSweepTimeoutSlashingAmount *big.Int - MovedFundsSweepTimeoutNotifierRewardMultiplier uint32 -} - -func (b *Bridge) MovingFundsParameters() (movingFundsParameters, error) { - result, err := b.contract.MovingFundsParameters( - b.callerOptions, - ) +// Transaction submission. +func (b *Bridge) UpdateDepositParameters( + arg_depositDustThreshold uint64, + arg_depositTreasuryFeeDivisor uint64, + arg_depositTxMaxFee uint64, + arg_depositRevealAheadPeriod uint32, - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "movingFundsParameters", - ) - } - - return result, err -} - -func (b *Bridge) MovingFundsParametersAtBlock( - blockNumber *big.Int, -) (movingFundsParameters, error) { - var result movingFundsParameters - - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "movingFundsParameters", - &result, + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction updateDepositParameters", + " params: ", + fmt.Sprint( + arg_depositDustThreshold, + arg_depositTreasuryFeeDivisor, + arg_depositTxMaxFee, + arg_depositRevealAheadPeriod, + ), ) - return result, err -} + b.transactionMutex.Lock() + defer b.transactionMutex.Unlock() -func (b *Bridge) PendingRedemptions( - arg_redemptionKey *big.Int, -) (abi.RedemptionRedemptionRequest, error) { - result, err := b.contract.PendingRedemptions( - b.callerOptions, - arg_redemptionKey, - ) + // create a copy + transactorOptions := new(bind.TransactOpts) + *transactorOptions = *b.transactorOptions - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "pendingRedemptions", - arg_redemptionKey, + if len(transactionOptions) > 1 { + return nil, fmt.Errorf( + "could not process multiple transaction options sets", ) + } else if len(transactionOptions) > 0 { + transactionOptions[0].Apply(transactorOptions) } - return result, err -} - -func (b *Bridge) PendingRedemptionsAtBlock( - arg_redemptionKey *big.Int, - blockNumber *big.Int, -) (abi.RedemptionRedemptionRequest, error) { - var result abi.RedemptionRedemptionRequest - - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "pendingRedemptions", - &result, - arg_redemptionKey, - ) - - return result, err -} + nonce, err := b.nonceManager.CurrentNonce() + if err != nil { + return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) + } -type redemptionParameters struct { - RedemptionDustThreshold uint64 - RedemptionTreasuryFeeDivisor uint64 - RedemptionTxMaxFee uint64 - RedemptionTxMaxTotalFee uint64 - RedemptionTimeout uint32 - RedemptionTimeoutSlashingAmount *big.Int - RedemptionTimeoutNotifierRewardMultiplier uint32 -} + transactorOptions.Nonce = new(big.Int).SetUint64(nonce) -func (b *Bridge) RedemptionParameters() (redemptionParameters, error) { - result, err := b.contract.RedemptionParameters( - b.callerOptions, + transaction, err := b.contract.UpdateDepositParameters( + transactorOptions, + arg_depositDustThreshold, + arg_depositTreasuryFeeDivisor, + arg_depositTxMaxFee, + arg_depositRevealAheadPeriod, ) - if err != nil { - return result, b.errorResolver.ResolveError( + return transaction, b.errorResolver.ResolveError( err, - b.callerOptions.From, + b.transactorOptions.From, nil, - "redemptionParameters", + "updateDepositParameters", + arg_depositDustThreshold, + arg_depositTreasuryFeeDivisor, + arg_depositTxMaxFee, + arg_depositRevealAheadPeriod, ) } - return result, err -} + bLogger.Infof( + "submitted transaction updateDepositParameters with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) -func (b *Bridge) RedemptionParametersAtBlock( - blockNumber *big.Int, -) (redemptionParameters, error) { - var result redemptionParameters + 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 + } - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "redemptionParameters", - &result, - ) + transaction, err := b.contract.UpdateDepositParameters( + newTransactorOptions, + arg_depositDustThreshold, + arg_depositTreasuryFeeDivisor, + arg_depositTxMaxFee, + arg_depositRevealAheadPeriod, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateDepositParameters", + arg_depositDustThreshold, + arg_depositTreasuryFeeDivisor, + arg_depositTxMaxFee, + arg_depositRevealAheadPeriod, + ) + } - return result, err -} + bLogger.Infof( + "submitted transaction updateDepositParameters with id: [%s] and nonce [%v]", + transaction.Hash(), + transaction.Nonce(), + ) -func (b *Bridge) SpentMainUTXOs( - arg_utxoKey *big.Int, -) (bool, error) { - result, err := b.contract.SpentMainUTXOs( - b.callerOptions, - arg_utxoKey, + return transaction, nil + }, ) - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "spentMainUTXOs", - arg_utxoKey, - ) - } + b.nonceManager.IncrementNonce() - return result, err + return transaction, err } -func (b *Bridge) SpentMainUTXOsAtBlock( - arg_utxoKey *big.Int, +// Non-mutating call, not a transaction submission. +func (b *Bridge) CallUpdateDepositParameters( + arg_depositDustThreshold uint64, + arg_depositTreasuryFeeDivisor uint64, + arg_depositTxMaxFee uint64, + arg_depositRevealAheadPeriod uint32, blockNumber *big.Int, -) (bool, error) { - var result bool +) error { + var result interface{} = nil err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, + b.transactorOptions.From, + blockNumber, nil, b.contractABI, b.caller, b.errorResolver, b.contractAddress, - "spentMainUTXOs", + "updateDepositParameters", &result, - arg_utxoKey, + arg_depositDustThreshold, + arg_depositTreasuryFeeDivisor, + arg_depositTxMaxFee, + arg_depositRevealAheadPeriod, ) - return result, err + return err } -func (b *Bridge) TimedOutRedemptions( - arg_redemptionKey *big.Int, -) (abi.RedemptionRedemptionRequest, error) { - result, err := b.contract.TimedOutRedemptions( - b.callerOptions, - arg_redemptionKey, - ) +func (b *Bridge) UpdateDepositParametersGasEstimate( + arg_depositDustThreshold uint64, + arg_depositTreasuryFeeDivisor uint64, + arg_depositTxMaxFee uint64, + arg_depositRevealAheadPeriod uint32, +) (uint64, error) { + var result uint64 - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "timedOutRedemptions", - arg_redemptionKey, - ) - } + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "updateDepositParameters", + b.contractABI, + b.transactor, + arg_depositDustThreshold, + arg_depositTreasuryFeeDivisor, + arg_depositTxMaxFee, + arg_depositRevealAheadPeriod, + ) return result, err } -func (b *Bridge) TimedOutRedemptionsAtBlock( - arg_redemptionKey *big.Int, +// Transaction submission. +func (b *Bridge) UpdateFraudParameters( + arg_fraudChallengeDepositAmount *big.Int, + arg_fraudChallengeDefeatTimeout uint32, + arg_fraudSlashingAmount *big.Int, + arg_fraudNotifierRewardMultiplier uint32, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction updateFraudParameters", + " params: ", + fmt.Sprint( + arg_fraudChallengeDepositAmount, + arg_fraudChallengeDefeatTimeout, + arg_fraudSlashingAmount, + arg_fraudNotifierRewardMultiplier, + ), + ) + + 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.UpdateFraudParameters( + transactorOptions, + arg_fraudChallengeDepositAmount, + arg_fraudChallengeDefeatTimeout, + arg_fraudSlashingAmount, + arg_fraudNotifierRewardMultiplier, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateFraudParameters", + arg_fraudChallengeDepositAmount, + arg_fraudChallengeDefeatTimeout, + arg_fraudSlashingAmount, + arg_fraudNotifierRewardMultiplier, + ) + } + + bLogger.Infof( + "submitted transaction updateFraudParameters 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.UpdateFraudParameters( + newTransactorOptions, + arg_fraudChallengeDepositAmount, + arg_fraudChallengeDefeatTimeout, + arg_fraudSlashingAmount, + arg_fraudNotifierRewardMultiplier, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateFraudParameters", + arg_fraudChallengeDepositAmount, + arg_fraudChallengeDefeatTimeout, + arg_fraudSlashingAmount, + arg_fraudNotifierRewardMultiplier, + ) + } + + bLogger.Infof( + "submitted transaction updateFraudParameters 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) CallUpdateFraudParameters( + arg_fraudChallengeDepositAmount *big.Int, + arg_fraudChallengeDefeatTimeout uint32, + arg_fraudSlashingAmount *big.Int, + arg_fraudNotifierRewardMultiplier uint32, blockNumber *big.Int, -) (abi.RedemptionRedemptionRequest, error) { - var result abi.RedemptionRedemptionRequest +) error { + var result interface{} = nil err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, + b.transactorOptions.From, + blockNumber, nil, b.contractABI, b.caller, b.errorResolver, b.contractAddress, - "timedOutRedemptions", + "updateFraudParameters", &result, - arg_redemptionKey, + arg_fraudChallengeDepositAmount, + arg_fraudChallengeDefeatTimeout, + arg_fraudSlashingAmount, + arg_fraudNotifierRewardMultiplier, + ) + + return err +} + +func (b *Bridge) UpdateFraudParametersGasEstimate( + arg_fraudChallengeDepositAmount *big.Int, + arg_fraudChallengeDefeatTimeout uint32, + arg_fraudSlashingAmount *big.Int, + arg_fraudNotifierRewardMultiplier uint32, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "updateFraudParameters", + b.contractABI, + b.transactor, + arg_fraudChallengeDepositAmount, + arg_fraudChallengeDefeatTimeout, + arg_fraudSlashingAmount, + arg_fraudNotifierRewardMultiplier, ) return result, err } -func (b *Bridge) Treasury() (common.Address, error) { - result, err := b.contract.Treasury( - b.callerOptions, +// Transaction submission. +func (b *Bridge) UpdateMovingFundsParameters( + arg_movingFundsTxMaxTotalFee uint64, + arg_movingFundsDustThreshold uint64, + arg_movingFundsTimeoutResetDelay uint32, + arg_movingFundsTimeout uint32, + arg_movingFundsTimeoutSlashingAmount *big.Int, + arg_movingFundsTimeoutNotifierRewardMultiplier uint32, + arg_movingFundsCommitmentGasOffset uint16, + arg_movedFundsSweepTxMaxTotalFee uint64, + arg_movedFundsSweepTimeout uint32, + arg_movedFundsSweepTimeoutSlashingAmount *big.Int, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier uint32, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction updateMovingFundsParameters", + " params: ", + fmt.Sprint( + arg_movingFundsTxMaxTotalFee, + arg_movingFundsDustThreshold, + arg_movingFundsTimeoutResetDelay, + arg_movingFundsTimeout, + arg_movingFundsTimeoutSlashingAmount, + arg_movingFundsTimeoutNotifierRewardMultiplier, + arg_movingFundsCommitmentGasOffset, + arg_movedFundsSweepTxMaxTotalFee, + arg_movedFundsSweepTimeout, + arg_movedFundsSweepTimeoutSlashingAmount, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + ), + ) + + 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.UpdateMovingFundsParameters( + transactorOptions, + arg_movingFundsTxMaxTotalFee, + arg_movingFundsDustThreshold, + arg_movingFundsTimeoutResetDelay, + arg_movingFundsTimeout, + arg_movingFundsTimeoutSlashingAmount, + arg_movingFundsTimeoutNotifierRewardMultiplier, + arg_movingFundsCommitmentGasOffset, + arg_movedFundsSweepTxMaxTotalFee, + arg_movedFundsSweepTimeout, + arg_movedFundsSweepTimeoutSlashingAmount, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateMovingFundsParameters", + arg_movingFundsTxMaxTotalFee, + arg_movingFundsDustThreshold, + arg_movingFundsTimeoutResetDelay, + arg_movingFundsTimeout, + arg_movingFundsTimeoutSlashingAmount, + arg_movingFundsTimeoutNotifierRewardMultiplier, + arg_movingFundsCommitmentGasOffset, + arg_movedFundsSweepTxMaxTotalFee, + arg_movedFundsSweepTimeout, + arg_movedFundsSweepTimeoutSlashingAmount, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + ) + } + + bLogger.Infof( + "submitted transaction updateMovingFundsParameters 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.UpdateMovingFundsParameters( + newTransactorOptions, + arg_movingFundsTxMaxTotalFee, + arg_movingFundsDustThreshold, + arg_movingFundsTimeoutResetDelay, + arg_movingFundsTimeout, + arg_movingFundsTimeoutSlashingAmount, + arg_movingFundsTimeoutNotifierRewardMultiplier, + arg_movingFundsCommitmentGasOffset, + arg_movedFundsSweepTxMaxTotalFee, + arg_movedFundsSweepTimeout, + arg_movedFundsSweepTimeoutSlashingAmount, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateMovingFundsParameters", + arg_movingFundsTxMaxTotalFee, + arg_movingFundsDustThreshold, + arg_movingFundsTimeoutResetDelay, + arg_movingFundsTimeout, + arg_movingFundsTimeoutSlashingAmount, + arg_movingFundsTimeoutNotifierRewardMultiplier, + arg_movingFundsCommitmentGasOffset, + arg_movedFundsSweepTxMaxTotalFee, + arg_movedFundsSweepTimeout, + arg_movedFundsSweepTimeoutSlashingAmount, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + ) + } + + bLogger.Infof( + "submitted transaction updateMovingFundsParameters 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) CallUpdateMovingFundsParameters( + arg_movingFundsTxMaxTotalFee uint64, + arg_movingFundsDustThreshold uint64, + arg_movingFundsTimeoutResetDelay uint32, + arg_movingFundsTimeout uint32, + arg_movingFundsTimeoutSlashingAmount *big.Int, + arg_movingFundsTimeoutNotifierRewardMultiplier uint32, + arg_movingFundsCommitmentGasOffset uint16, + arg_movedFundsSweepTxMaxTotalFee uint64, + arg_movedFundsSweepTimeout uint32, + arg_movedFundsSweepTimeoutSlashingAmount *big.Int, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier uint32, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "updateMovingFundsParameters", + &result, + arg_movingFundsTxMaxTotalFee, + arg_movingFundsDustThreshold, + arg_movingFundsTimeoutResetDelay, + arg_movingFundsTimeout, + arg_movingFundsTimeoutSlashingAmount, + arg_movingFundsTimeoutNotifierRewardMultiplier, + arg_movingFundsCommitmentGasOffset, + arg_movedFundsSweepTxMaxTotalFee, + arg_movedFundsSweepTimeout, + arg_movedFundsSweepTimeoutSlashingAmount, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + ) + + return err +} + +func (b *Bridge) UpdateMovingFundsParametersGasEstimate( + arg_movingFundsTxMaxTotalFee uint64, + arg_movingFundsDustThreshold uint64, + arg_movingFundsTimeoutResetDelay uint32, + arg_movingFundsTimeout uint32, + arg_movingFundsTimeoutSlashingAmount *big.Int, + arg_movingFundsTimeoutNotifierRewardMultiplier uint32, + arg_movingFundsCommitmentGasOffset uint16, + arg_movedFundsSweepTxMaxTotalFee uint64, + arg_movedFundsSweepTimeout uint32, + arg_movedFundsSweepTimeoutSlashingAmount *big.Int, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier uint32, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "updateMovingFundsParameters", + b.contractABI, + b.transactor, + arg_movingFundsTxMaxTotalFee, + arg_movingFundsDustThreshold, + arg_movingFundsTimeoutResetDelay, + arg_movingFundsTimeout, + arg_movingFundsTimeoutSlashingAmount, + arg_movingFundsTimeoutNotifierRewardMultiplier, + arg_movingFundsCommitmentGasOffset, + arg_movedFundsSweepTxMaxTotalFee, + arg_movedFundsSweepTimeout, + arg_movedFundsSweepTimeoutSlashingAmount, + arg_movedFundsSweepTimeoutNotifierRewardMultiplier, + ) + + return result, err +} + +// Transaction submission. +func (b *Bridge) UpdateRedemptionParameters( + arg_redemptionDustThreshold uint64, + arg_redemptionTreasuryFeeDivisor uint64, + arg_redemptionTxMaxFee uint64, + arg_redemptionTxMaxTotalFee uint64, + arg_redemptionTimeout uint32, + arg_redemptionTimeoutSlashingAmount *big.Int, + arg_redemptionTimeoutNotifierRewardMultiplier uint32, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction updateRedemptionParameters", + " params: ", + fmt.Sprint( + arg_redemptionDustThreshold, + arg_redemptionTreasuryFeeDivisor, + arg_redemptionTxMaxFee, + arg_redemptionTxMaxTotalFee, + arg_redemptionTimeout, + arg_redemptionTimeoutSlashingAmount, + arg_redemptionTimeoutNotifierRewardMultiplier, + ), + ) + + 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.UpdateRedemptionParameters( + transactorOptions, + arg_redemptionDustThreshold, + arg_redemptionTreasuryFeeDivisor, + arg_redemptionTxMaxFee, + arg_redemptionTxMaxTotalFee, + arg_redemptionTimeout, + arg_redemptionTimeoutSlashingAmount, + arg_redemptionTimeoutNotifierRewardMultiplier, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateRedemptionParameters", + arg_redemptionDustThreshold, + arg_redemptionTreasuryFeeDivisor, + arg_redemptionTxMaxFee, + arg_redemptionTxMaxTotalFee, + arg_redemptionTimeout, + arg_redemptionTimeoutSlashingAmount, + arg_redemptionTimeoutNotifierRewardMultiplier, + ) + } + + bLogger.Infof( + "submitted transaction updateRedemptionParameters 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.UpdateRedemptionParameters( + newTransactorOptions, + arg_redemptionDustThreshold, + arg_redemptionTreasuryFeeDivisor, + arg_redemptionTxMaxFee, + arg_redemptionTxMaxTotalFee, + arg_redemptionTimeout, + arg_redemptionTimeoutSlashingAmount, + arg_redemptionTimeoutNotifierRewardMultiplier, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateRedemptionParameters", + arg_redemptionDustThreshold, + arg_redemptionTreasuryFeeDivisor, + arg_redemptionTxMaxFee, + arg_redemptionTxMaxTotalFee, + arg_redemptionTimeout, + arg_redemptionTimeoutSlashingAmount, + arg_redemptionTimeoutNotifierRewardMultiplier, + ) + } + + bLogger.Infof( + "submitted transaction updateRedemptionParameters 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) CallUpdateRedemptionParameters( + arg_redemptionDustThreshold uint64, + arg_redemptionTreasuryFeeDivisor uint64, + arg_redemptionTxMaxFee uint64, + arg_redemptionTxMaxTotalFee uint64, + arg_redemptionTimeout uint32, + arg_redemptionTimeoutSlashingAmount *big.Int, + arg_redemptionTimeoutNotifierRewardMultiplier uint32, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "updateRedemptionParameters", + &result, + arg_redemptionDustThreshold, + arg_redemptionTreasuryFeeDivisor, + arg_redemptionTxMaxFee, + arg_redemptionTxMaxTotalFee, + arg_redemptionTimeout, + arg_redemptionTimeoutSlashingAmount, + arg_redemptionTimeoutNotifierRewardMultiplier, + ) + + return err +} + +func (b *Bridge) UpdateRedemptionParametersGasEstimate( + arg_redemptionDustThreshold uint64, + arg_redemptionTreasuryFeeDivisor uint64, + arg_redemptionTxMaxFee uint64, + arg_redemptionTxMaxTotalFee uint64, + arg_redemptionTimeout uint32, + arg_redemptionTimeoutSlashingAmount *big.Int, + arg_redemptionTimeoutNotifierRewardMultiplier uint32, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "updateRedemptionParameters", + b.contractABI, + b.transactor, + arg_redemptionDustThreshold, + arg_redemptionTreasuryFeeDivisor, + arg_redemptionTxMaxFee, + arg_redemptionTxMaxTotalFee, + arg_redemptionTimeout, + arg_redemptionTimeoutSlashingAmount, + arg_redemptionTimeoutNotifierRewardMultiplier, + ) + + return result, err +} + +// Transaction submission. +func (b *Bridge) UpdateTreasury( + arg_treasury common.Address, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction updateTreasury", + " params: ", + fmt.Sprint( + arg_treasury, + ), + ) + + 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.UpdateTreasury( + transactorOptions, + arg_treasury, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateTreasury", + arg_treasury, + ) + } + + bLogger.Infof( + "submitted transaction updateTreasury 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.UpdateTreasury( + newTransactorOptions, + arg_treasury, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateTreasury", + arg_treasury, + ) + } + + bLogger.Infof( + "submitted transaction updateTreasury 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) CallUpdateTreasury( + arg_treasury 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, + "updateTreasury", + &result, + arg_treasury, + ) + + return err +} + +func (b *Bridge) UpdateTreasuryGasEstimate( + arg_treasury common.Address, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "updateTreasury", + b.contractABI, + b.transactor, + arg_treasury, + ) + + return result, err +} + +// Transaction submission. +func (b *Bridge) UpdateWalletParameters( + arg_walletCreationPeriod uint32, + arg_walletCreationMinBtcBalance uint64, + arg_walletCreationMaxBtcBalance uint64, + arg_walletClosureMinBtcBalance uint64, + arg_walletMaxAge uint32, + arg_walletMaxBtcTransfer uint64, + arg_walletClosingPeriod uint32, + + transactionOptions ...chainutil.TransactionOptions, +) (*types.Transaction, error) { + bLogger.Debug( + "submitting transaction updateWalletParameters", + " params: ", + fmt.Sprint( + arg_walletCreationPeriod, + arg_walletCreationMinBtcBalance, + arg_walletCreationMaxBtcBalance, + arg_walletClosureMinBtcBalance, + arg_walletMaxAge, + arg_walletMaxBtcTransfer, + arg_walletClosingPeriod, + ), + ) + + 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.UpdateWalletParameters( + transactorOptions, + arg_walletCreationPeriod, + arg_walletCreationMinBtcBalance, + arg_walletCreationMaxBtcBalance, + arg_walletClosureMinBtcBalance, + arg_walletMaxAge, + arg_walletMaxBtcTransfer, + arg_walletClosingPeriod, + ) + if err != nil { + return transaction, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateWalletParameters", + arg_walletCreationPeriod, + arg_walletCreationMinBtcBalance, + arg_walletCreationMaxBtcBalance, + arg_walletClosureMinBtcBalance, + arg_walletMaxAge, + arg_walletMaxBtcTransfer, + arg_walletClosingPeriod, + ) + } + + bLogger.Infof( + "submitted transaction updateWalletParameters 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.UpdateWalletParameters( + newTransactorOptions, + arg_walletCreationPeriod, + arg_walletCreationMinBtcBalance, + arg_walletCreationMaxBtcBalance, + arg_walletClosureMinBtcBalance, + arg_walletMaxAge, + arg_walletMaxBtcTransfer, + arg_walletClosingPeriod, + ) + if err != nil { + return nil, b.errorResolver.ResolveError( + err, + b.transactorOptions.From, + nil, + "updateWalletParameters", + arg_walletCreationPeriod, + arg_walletCreationMinBtcBalance, + arg_walletCreationMaxBtcBalance, + arg_walletClosureMinBtcBalance, + arg_walletMaxAge, + arg_walletMaxBtcTransfer, + arg_walletClosingPeriod, + ) + } + + bLogger.Infof( + "submitted transaction updateWalletParameters 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) CallUpdateWalletParameters( + arg_walletCreationPeriod uint32, + arg_walletCreationMinBtcBalance uint64, + arg_walletCreationMaxBtcBalance uint64, + arg_walletClosureMinBtcBalance uint64, + arg_walletMaxAge uint32, + arg_walletMaxBtcTransfer uint64, + arg_walletClosingPeriod uint32, + blockNumber *big.Int, +) error { + var result interface{} = nil + + err := chainutil.CallAtBlock( + b.transactorOptions.From, + blockNumber, nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "updateWalletParameters", + &result, + arg_walletCreationPeriod, + arg_walletCreationMinBtcBalance, + arg_walletCreationMaxBtcBalance, + arg_walletClosureMinBtcBalance, + arg_walletMaxAge, + arg_walletMaxBtcTransfer, + arg_walletClosingPeriod, + ) + + return err +} + +func (b *Bridge) UpdateWalletParametersGasEstimate( + arg_walletCreationPeriod uint32, + arg_walletCreationMinBtcBalance uint64, + arg_walletCreationMaxBtcBalance uint64, + arg_walletClosureMinBtcBalance uint64, + arg_walletMaxAge uint32, + arg_walletMaxBtcTransfer uint64, + arg_walletClosingPeriod uint32, +) (uint64, error) { + var result uint64 + + result, err := chainutil.EstimateGas( + b.callerOptions.From, + b.contractAddress, + "updateWalletParameters", + b.contractABI, + b.transactor, + arg_walletCreationPeriod, + arg_walletCreationMinBtcBalance, + arg_walletCreationMaxBtcBalance, + arg_walletClosureMinBtcBalance, + arg_walletMaxAge, + arg_walletMaxBtcTransfer, + arg_walletClosingPeriod, + ) + + return result, err +} + +// ----- Const Methods ------ + +func (b *Bridge) ActiveWalletID() ([32]byte, error) { + result, err := b.contract.ActiveWalletID( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "activeWalletID", + ) + } + + return result, err +} + +func (b *Bridge) ActiveWalletIDAtBlock( + 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, + "activeWalletID", + &result, + ) + + return result, err +} + +func (b *Bridge) ActiveWalletPubKeyHash() ([20]byte, error) { + result, err := b.contract.ActiveWalletPubKeyHash( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "activeWalletPubKeyHash", + ) + } + + return result, err +} + +func (b *Bridge) ActiveWalletPubKeyHashAtBlock( + 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, + "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, + ) + + return result, err +} + +type depositParameters struct { + DepositDustThreshold uint64 + DepositTreasuryFeeDivisor uint64 + DepositTxMaxFee uint64 + DepositRevealAheadPeriod uint32 +} + +func (b *Bridge) DepositParameters() (depositParameters, error) { + result, err := b.contract.DepositParameters( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "depositParameters", + ) + } + + return result, err +} + +func (b *Bridge) DepositParametersAtBlock( + blockNumber *big.Int, +) (depositParameters, error) { + var result depositParameters + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "depositParameters", + &result, + ) + + return result, err +} + +func (b *Bridge) Deposits( + arg_depositKey *big.Int, +) (abi.DepositDepositRequest, error) { + result, err := b.contract.Deposits( + b.callerOptions, + arg_depositKey, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "deposits", + arg_depositKey, + ) + } + + return result, err +} + +func (b *Bridge) DepositsAtBlock( + arg_depositKey *big.Int, + blockNumber *big.Int, +) (abi.DepositDepositRequest, error) { + var result abi.DepositDepositRequest + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "deposits", + &result, + arg_depositKey, + ) + + return result, err +} + +func (b *Bridge) EcdsaFraudRouter() (common.Address, error) { + result, err := b.contract.EcdsaFraudRouter( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "ecdsaFraudRouter", + ) + } + + return result, err +} + +func (b *Bridge) EcdsaFraudRouterAtBlock( + 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, + "ecdsaFraudRouter", + &result, + ) + + return result, err +} + +func (b *Bridge) EcdsaRetired() (bool, error) { + result, err := b.contract.EcdsaRetired( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "ecdsaRetired", + ) + } + + return result, err +} + +func (b *Bridge) EcdsaRetiredAtBlock( + blockNumber *big.Int, +) (bool, error) { + var result bool + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "ecdsaRetired", + &result, + ) + + return result, err +} + +type fraudParameters struct { + FraudChallengeDepositAmount *big.Int + FraudChallengeDefeatTimeout uint32 + FraudSlashingAmount *big.Int + FraudNotifierRewardMultiplier uint32 +} + +func (b *Bridge) FraudParameters() (fraudParameters, error) { + result, err := b.contract.FraudParameters( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "fraudParameters", + ) + } + + return result, err +} + +func (b *Bridge) FraudParametersAtBlock( + blockNumber *big.Int, +) (fraudParameters, error) { + var result fraudParameters + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "fraudParameters", + &result, + ) + + return result, err +} + +type frostLifecycleContext struct { + FrostRegistry common.Address + WalletID [32]byte +} + +func (b *Bridge) FrostLifecycleContext( + arg_walletPubKeyHash [20]byte, +) (frostLifecycleContext, error) { + result, err := b.contract.FrostLifecycleContext( + b.callerOptions, + arg_walletPubKeyHash, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "frostLifecycleContext", + arg_walletPubKeyHash, + ) + } + + return result, err +} + +func (b *Bridge) FrostLifecycleContextAtBlock( + arg_walletPubKeyHash [20]byte, + blockNumber *big.Int, +) (frostLifecycleContext, error) { + var result frostLifecycleContext + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "frostLifecycleContext", + &result, + arg_walletPubKeyHash, + ) + + 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, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "getRedemptionWatchtower", + ) + } + + return result, err +} + +func (b *Bridge) GetRedemptionWatchtowerAtBlock( + 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, + "getRedemptionWatchtower", + &result, + ) + + return result, err +} + +func (b *Bridge) Governance() (common.Address, error) { + result, err := b.contract.Governance( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "governance", + ) + } + + return result, err +} + +func (b *Bridge) GovernanceAtBlock( + 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, + "governance", + &result, + ) + + return result, err +} + +func (b *Bridge) IsVaultTrusted( + arg_vault common.Address, +) (bool, error) { + result, err := b.contract.IsVaultTrusted( + b.callerOptions, + arg_vault, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "isVaultTrusted", + arg_vault, + ) + } + + return result, err +} + +func (b *Bridge) IsVaultTrustedAtBlock( + arg_vault common.Address, + blockNumber *big.Int, +) (bool, error) { + var result bool + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "isVaultTrusted", + &result, + arg_vault, + ) + + return result, err +} + +func (b *Bridge) LiveWalletsCount() (uint32, error) { + result, err := b.contract.LiveWalletsCount( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "liveWalletsCount", + ) + } + + return result, err +} + +func (b *Bridge) LiveWalletsCountAtBlock( + blockNumber *big.Int, +) (uint32, error) { + var result uint32 + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "liveWalletsCount", + &result, + ) + + return result, err +} + +func (b *Bridge) MovedFundsSweepRequests( + arg_requestKey *big.Int, +) (abi.MovingFundsMovedFundsSweepRequest, error) { + result, err := b.contract.MovedFundsSweepRequests( + b.callerOptions, + arg_requestKey, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "movedFundsSweepRequests", + arg_requestKey, + ) + } + + return result, err +} + +func (b *Bridge) MovedFundsSweepRequestsAtBlock( + arg_requestKey *big.Int, + blockNumber *big.Int, +) (abi.MovingFundsMovedFundsSweepRequest, error) { + var result abi.MovingFundsMovedFundsSweepRequest + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "movedFundsSweepRequests", + &result, + arg_requestKey, + ) + + return result, err +} + +type movingFundsParameters struct { + MovingFundsTxMaxTotalFee uint64 + MovingFundsDustThreshold uint64 + MovingFundsTimeoutResetDelay uint32 + MovingFundsTimeout uint32 + MovingFundsTimeoutSlashingAmount *big.Int + MovingFundsTimeoutNotifierRewardMultiplier uint32 + MovingFundsCommitmentGasOffset uint16 + MovedFundsSweepTxMaxTotalFee uint64 + MovedFundsSweepTimeout uint32 + MovedFundsSweepTimeoutSlashingAmount *big.Int + MovedFundsSweepTimeoutNotifierRewardMultiplier uint32 +} + +func (b *Bridge) MovingFundsParameters() (movingFundsParameters, error) { + result, err := b.contract.MovingFundsParameters( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "movingFundsParameters", + ) + } + + return result, err +} + +func (b *Bridge) MovingFundsParametersAtBlock( + blockNumber *big.Int, +) (movingFundsParameters, error) { + var result movingFundsParameters + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "movingFundsParameters", + &result, + ) + + return result, err +} + +func (b *Bridge) P2trFraudRouter() (common.Address, error) { + result, err := b.contract.P2trFraudRouter( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "p2trFraudRouter", + ) + } + + return result, err +} + +func (b *Bridge) P2trFraudRouterAtBlock( + 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, + "p2trFraudRouter", + &result, + ) + + return result, err +} + +func (b *Bridge) PendingRedemptions( + arg_redemptionKey *big.Int, +) (abi.RedemptionRedemptionRequest, error) { + result, err := b.contract.PendingRedemptions( + b.callerOptions, + arg_redemptionKey, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "pendingRedemptions", + arg_redemptionKey, + ) + } + + return result, err +} + +func (b *Bridge) PendingRedemptionsAtBlock( + arg_redemptionKey *big.Int, + blockNumber *big.Int, +) (abi.RedemptionRedemptionRequest, error) { + var result abi.RedemptionRedemptionRequest + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "pendingRedemptions", + &result, + arg_redemptionKey, + ) + + return result, err +} + +type redemptionParameters struct { + RedemptionDustThreshold uint64 + RedemptionTreasuryFeeDivisor uint64 + RedemptionTxMaxFee uint64 + RedemptionTxMaxTotalFee uint64 + RedemptionTimeout uint32 + RedemptionTimeoutSlashingAmount *big.Int + RedemptionTimeoutNotifierRewardMultiplier uint32 +} + +func (b *Bridge) RedemptionParameters() (redemptionParameters, error) { + result, err := b.contract.RedemptionParameters( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "redemptionParameters", + ) + } + + return result, err +} + +func (b *Bridge) RedemptionParametersAtBlock( + blockNumber *big.Int, +) (redemptionParameters, error) { + var result redemptionParameters + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "redemptionParameters", + &result, + ) + + return result, err +} + +func (b *Bridge) SpentMainUTXOs( + arg_utxoKey *big.Int, +) (bool, error) { + result, err := b.contract.SpentMainUTXOs( + b.callerOptions, + arg_utxoKey, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "spentMainUTXOs", + arg_utxoKey, + ) + } + + return result, err +} + +func (b *Bridge) SpentMainUTXOsAtBlock( + arg_utxoKey *big.Int, + blockNumber *big.Int, +) (bool, error) { + var result bool + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "spentMainUTXOs", + &result, + arg_utxoKey, + ) + + return result, err +} + +func (b *Bridge) TimedOutRedemptions( + arg_redemptionKey *big.Int, +) (abi.RedemptionRedemptionRequest, error) { + result, err := b.contract.TimedOutRedemptions( + b.callerOptions, + arg_redemptionKey, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "timedOutRedemptions", + arg_redemptionKey, + ) + } + + return result, err +} + +func (b *Bridge) TimedOutRedemptionsAtBlock( + arg_redemptionKey *big.Int, + blockNumber *big.Int, +) (abi.RedemptionRedemptionRequest, error) { + var result abi.RedemptionRedemptionRequest + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "timedOutRedemptions", + &result, + arg_redemptionKey, + ) + + return result, err +} + +func (b *Bridge) Treasury() (common.Address, error) { + result, err := b.contract.Treasury( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "treasury", + ) + } + + return result, err +} + +func (b *Bridge) TreasuryAtBlock( + 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, + "treasury", + &result, + ) + + return result, err +} + +func (b *Bridge) TxProofDifficultyFactor() (*big.Int, error) { + result, err := b.contract.TxProofDifficultyFactor( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "txProofDifficultyFactor", + ) + } + + return result, err +} + +func (b *Bridge) TxProofDifficultyFactorAtBlock( + blockNumber *big.Int, +) (*big.Int, error) { + var result *big.Int + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "txProofDifficultyFactor", + &result, + ) + + 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 + WalletCreationMaxBtcBalance uint64 + WalletClosureMinBtcBalance uint64 + WalletMaxAge uint32 + WalletMaxBtcTransfer uint64 + WalletClosingPeriod uint32 +} + +func (b *Bridge) WalletParameters() (walletParameters, error) { + result, err := b.contract.WalletParameters( + b.callerOptions, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "walletParameters", + ) + } + + return result, err +} + +func (b *Bridge) WalletParametersAtBlock( + blockNumber *big.Int, +) (walletParameters, error) { + var result walletParameters + + err := chainutil.CallAtBlock( + b.callerOptions.From, + blockNumber, + nil, + b.contractABI, + b.caller, + b.errorResolver, + b.contractAddress, + "walletParameters", + &result, + ) + + 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) { + result, err := b.contract.Wallets( + b.callerOptions, + arg_walletPubKeyHash, + ) + + if err != nil { + return result, b.errorResolver.ResolveError( + err, + b.callerOptions.From, + nil, + "wallets", + arg_walletPubKeyHash, + ) + } + + return result, err +} + +func (b *Bridge) WalletsAtBlock( + arg_walletPubKeyHash [20]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, + "wallets", + &result, + arg_walletPubKeyHash, + ) + + 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( + opts *ethereum.SubscribeOpts, +) *BDepositParametersUpdatedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BDepositParametersUpdatedSubscription{ + b, + opts, + } +} + +type BDepositParametersUpdatedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts +} + +type bridgeDepositParametersUpdatedFunc func( + DepositDustThreshold uint64, + DepositTreasuryFeeDivisor uint64, + DepositTxMaxFee uint64, + DepositRevealAheadPeriod uint32, + blockNumber uint64, +) + +func (dpus *BDepositParametersUpdatedSubscription) OnEvent( + handler bridgeDepositParametersUpdatedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeDepositParametersUpdated) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.DepositDustThreshold, + event.DepositTreasuryFeeDivisor, + event.DepositTxMaxFee, + event.DepositRevealAheadPeriod, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := dpus.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (dpus *BDepositParametersUpdatedSubscription) Pipe( + sink chan *abi.BridgeDepositParametersUpdated, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(dpus.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := dpus.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - dpus.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past DepositParametersUpdated events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := dpus.contract.PastDepositParametersUpdatedEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past DepositParametersUpdated events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := dpus.contract.watchDepositParametersUpdated( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchDepositParametersUpdated( + sink chan *abi.BridgeDepositParametersUpdated, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchDepositParametersUpdated( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event DepositParametersUpdated had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event DepositParametersUpdated failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastDepositParametersUpdatedEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeDepositParametersUpdated, error) { + iterator, err := b.contract.FilterDepositParametersUpdated( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past DepositParametersUpdated events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeDepositParametersUpdated, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (b *Bridge) DepositRevealedEvent( + opts *ethereum.SubscribeOpts, + depositorFilter []common.Address, + walletPubKeyHashFilter [][20]byte, +) *BDepositRevealedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BDepositRevealedSubscription{ + b, + opts, + depositorFilter, + walletPubKeyHashFilter, + } +} + +type BDepositRevealedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + depositorFilter []common.Address + walletPubKeyHashFilter [][20]byte +} + +type bridgeDepositRevealedFunc func( + FundingTxHash [32]byte, + FundingOutputIndex uint32, + Depositor common.Address, + Amount uint64, + BlindingFactor [8]byte, + WalletPubKeyHash [20]byte, + RefundPubKeyHash [20]byte, + RefundLocktime [4]byte, + Vault common.Address, + blockNumber uint64, +) + +func (drs *BDepositRevealedSubscription) OnEvent( + handler bridgeDepositRevealedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeDepositRevealed) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.FundingTxHash, + event.FundingOutputIndex, + event.Depositor, + event.Amount, + event.BlindingFactor, + event.WalletPubKeyHash, + event.RefundPubKeyHash, + event.RefundLocktime, + event.Vault, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := drs.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (drs *BDepositRevealedSubscription) Pipe( + sink chan *abi.BridgeDepositRevealed, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(drs.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := drs.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - drs.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past DepositRevealed events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := drs.contract.PastDepositRevealedEvents( + fromBlock, + nil, + drs.depositorFilter, + drs.walletPubKeyHashFilter, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past DepositRevealed events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := drs.contract.watchDepositRevealed( + sink, + drs.depositorFilter, + drs.walletPubKeyHashFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchDepositRevealed( + sink chan *abi.BridgeDepositRevealed, + depositorFilter []common.Address, + walletPubKeyHashFilter [][20]byte, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchDepositRevealed( + &bind.WatchOpts{Context: ctx}, + sink, + depositorFilter, + walletPubKeyHashFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event DepositRevealed had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event DepositRevealed failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastDepositRevealedEvents( + startBlock uint64, + endBlock *uint64, + depositorFilter []common.Address, + walletPubKeyHashFilter [][20]byte, +) ([]*abi.BridgeDepositRevealed, error) { + iterator, err := b.contract.FilterDepositRevealed( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + depositorFilter, + walletPubKeyHashFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past DepositRevealed events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeDepositRevealed, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (b *Bridge) DepositVaultFixedEvent( + opts *ethereum.SubscribeOpts, + depositKeyFilter []*big.Int, +) *BDepositVaultFixedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BDepositVaultFixedSubscription{ + b, + opts, + depositKeyFilter, + } +} + +type BDepositVaultFixedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + depositKeyFilter []*big.Int +} + +type bridgeDepositVaultFixedFunc func( + DepositKey *big.Int, + NewVault common.Address, + blockNumber uint64, +) + +func (dvfs *BDepositVaultFixedSubscription) OnEvent( + handler bridgeDepositVaultFixedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeDepositVaultFixed) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.DepositKey, + event.NewVault, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := dvfs.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (dvfs *BDepositVaultFixedSubscription) Pipe( + sink chan *abi.BridgeDepositVaultFixed, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(dvfs.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := dvfs.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - dvfs.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past DepositVaultFixed events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := dvfs.contract.PastDepositVaultFixedEvents( + fromBlock, + nil, + dvfs.depositKeyFilter, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past DepositVaultFixed events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := dvfs.contract.watchDepositVaultFixed( + sink, + dvfs.depositKeyFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchDepositVaultFixed( + sink chan *abi.BridgeDepositVaultFixed, + depositKeyFilter []*big.Int, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchDepositVaultFixed( + &bind.WatchOpts{Context: ctx}, + sink, + depositKeyFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event DepositVaultFixed had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event DepositVaultFixed failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastDepositVaultFixedEvents( + startBlock uint64, + endBlock *uint64, + 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 DepositVaultFixed events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeDepositVaultFixed, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (b *Bridge) DepositsSweptEvent( + opts *ethereum.SubscribeOpts, +) *BDepositsSweptSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BDepositsSweptSubscription{ + b, + opts, + } +} + +type BDepositsSweptSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts +} + +type bridgeDepositsSweptFunc func( + WalletPubKeyHash [20]byte, + SweepTxHash [32]byte, + blockNumber uint64, +) + +func (dss *BDepositsSweptSubscription) OnEvent( + handler bridgeDepositsSweptFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeDepositsSwept) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.WalletPubKeyHash, + event.SweepTxHash, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := dss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (dss *BDepositsSweptSubscription) Pipe( + sink chan *abi.BridgeDepositsSwept, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(dss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := dss.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - dss.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past DepositsSwept events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := dss.contract.PastDepositsSweptEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "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) EcdsaFraudRouterSetEvent( + opts *ethereum.SubscribeOpts, +) *BEcdsaFraudRouterSetSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BEcdsaFraudRouterSetSubscription{ + b, + opts, + } +} + +type BEcdsaFraudRouterSetSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts +} + +type bridgeEcdsaFraudRouterSetFunc func( + EcdsaFraudRouter common.Address, + blockNumber uint64, +) + +func (efrss *BEcdsaFraudRouterSetSubscription) OnEvent( + handler bridgeEcdsaFraudRouterSetFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeEcdsaFraudRouterSet) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.EcdsaFraudRouter, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := efrss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (efrss *BEcdsaFraudRouterSetSubscription) Pipe( + sink chan *abi.BridgeEcdsaFraudRouterSet, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(efrss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := efrss.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - efrss.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past EcdsaFraudRouterSet events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := efrss.contract.PastEcdsaFraudRouterSetEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past EcdsaFraudRouterSet events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := efrss.contract.watchEcdsaFraudRouterSet( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchEcdsaFraudRouterSet( + sink chan *abi.BridgeEcdsaFraudRouterSet, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchEcdsaFraudRouterSet( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event EcdsaFraudRouterSet had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event EcdsaFraudRouterSet failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastEcdsaFraudRouterSetEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeEcdsaFraudRouterSet, error) { + iterator, err := b.contract.FilterEcdsaFraudRouterSet( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past EcdsaFraudRouterSet events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeEcdsaFraudRouterSet, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (b *Bridge) EcdsaRetiredEvent( + opts *ethereum.SubscribeOpts, +) *BEcdsaRetiredSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BEcdsaRetiredSubscription{ + b, + opts, + } +} + +type BEcdsaRetiredSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts +} + +type bridgeEcdsaRetiredFunc func( + blockNumber uint64, +) + +func (ers *BEcdsaRetiredSubscription) OnEvent( + handler bridgeEcdsaRetiredFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeEcdsaRetired) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.Raw.BlockNumber, + ) + } + } + }() + + sub := ers.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (ers *BEcdsaRetiredSubscription) Pipe( + sink chan *abi.BridgeEcdsaRetired, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(ers.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := ers.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - ers.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past EcdsaRetired events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := ers.contract.PastEcdsaRetiredEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past EcdsaRetired events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := ers.contract.watchEcdsaRetired( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchEcdsaRetired( + sink chan *abi.BridgeEcdsaRetired, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchEcdsaRetired( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event EcdsaRetired had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event EcdsaRetired failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastEcdsaRetiredEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeEcdsaRetired, error) { + iterator, err := b.contract.FilterEcdsaRetired( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past EcdsaRetired events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeEcdsaRetired, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + +func (b *Bridge) FraudParametersUpdatedEvent( + opts *ethereum.SubscribeOpts, +) *BFraudParametersUpdatedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BFraudParametersUpdatedSubscription{ + b, + opts, + } +} + +type BFraudParametersUpdatedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts +} + +type bridgeFraudParametersUpdatedFunc func( + FraudChallengeDepositAmount *big.Int, + FraudChallengeDefeatTimeout uint32, + FraudSlashingAmount *big.Int, + FraudNotifierRewardMultiplier uint32, + blockNumber uint64, +) + +func (fpus *BFraudParametersUpdatedSubscription) OnEvent( + handler bridgeFraudParametersUpdatedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeFraudParametersUpdated) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.FraudChallengeDepositAmount, + event.FraudChallengeDefeatTimeout, + event.FraudSlashingAmount, + event.FraudNotifierRewardMultiplier, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := fpus.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (fpus *BFraudParametersUpdatedSubscription) Pipe( + sink chan *abi.BridgeFraudParametersUpdated, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(fpus.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := fpus.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - fpus.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past FraudParametersUpdated events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := fpus.contract.PastFraudParametersUpdatedEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past FraudParametersUpdated events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := fpus.contract.watchFraudParametersUpdated( + sink, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchFraudParametersUpdated( + sink chan *abi.BridgeFraudParametersUpdated, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchFraudParametersUpdated( + &bind.WatchOpts{Context: ctx}, + sink, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event FraudParametersUpdated had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event FraudParametersUpdated failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastFraudParametersUpdatedEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeFraudParametersUpdated, error) { + iterator, err := b.contract.FilterFraudParametersUpdated( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, ) - if err != nil { - return result, b.errorResolver.ResolveError( + return nil, fmt.Errorf( + "error retrieving past FraudParametersUpdated events: [%v]", err, - b.callerOptions.From, - nil, - "treasury", ) } - return result, err -} - -func (b *Bridge) TreasuryAtBlock( - blockNumber *big.Int, -) (common.Address, error) { - var result common.Address + events := make([]*abi.BridgeFraudParametersUpdated, 0) - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "treasury", - &result, - ) + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } - return result, err + return events, nil } -func (b *Bridge) TxProofDifficultyFactor() (*big.Int, error) { - result, err := b.contract.TxProofDifficultyFactor( - b.callerOptions, - ) +func (b *Bridge) FrostWalletRegistrySetEvent( + opts *ethereum.SubscribeOpts, +) *BFrostWalletRegistrySetSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "txProofDifficultyFactor", - ) + return &BFrostWalletRegistrySetSubscription{ + b, + opts, } +} - return result, err +type BFrostWalletRegistrySetSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts } -func (b *Bridge) TxProofDifficultyFactorAtBlock( - blockNumber *big.Int, -) (*big.Int, error) { - var result *big.Int +type bridgeFrostWalletRegistrySetFunc func( + FrostWalletRegistry common.Address, + blockNumber uint64, +) - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "txProofDifficultyFactor", - &result, - ) +func (fwrss *BFrostWalletRegistrySetSubscription) OnEvent( + handler bridgeFrostWalletRegistrySetFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeFrostWalletRegistrySet) + ctx, cancelCtx := context.WithCancel(context.Background()) - return result, err -} + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.FrostWalletRegistry, + event.Raw.BlockNumber, + ) + } + } + }() -type walletParameters struct { - WalletCreationPeriod uint32 - WalletCreationMinBtcBalance uint64 - WalletCreationMaxBtcBalance uint64 - WalletClosureMinBtcBalance uint64 - WalletMaxAge uint32 - WalletMaxBtcTransfer uint64 - WalletClosingPeriod uint32 + sub := fwrss.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) } -func (b *Bridge) WalletParameters() (walletParameters, error) { - result, err := b.contract.WalletParameters( - b.callerOptions, +func (fwrss *BFrostWalletRegistrySetSubscription) Pipe( + sink chan *abi.BridgeFrostWalletRegistrySet, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(fwrss.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := fwrss.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - fwrss.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past FrostWalletRegistrySet events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := fwrss.contract.PastFrostWalletRegistrySetEvents( + fromBlock, + nil, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past FrostWalletRegistrySet events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := fwrss.contract.watchFrostWalletRegistrySet( + sink, ) - if err != nil { - return result, b.errorResolver.ResolveError( - err, - b.callerOptions.From, - nil, - "walletParameters", + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchFrostWalletRegistrySet( + sink chan *abi.BridgeFrostWalletRegistrySet, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchFrostWalletRegistrySet( + &bind.WatchOpts{Context: ctx}, + sink, ) } - return result, err -} + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event FrostWalletRegistrySet had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } -func (b *Bridge) WalletParametersAtBlock( - blockNumber *big.Int, -) (walletParameters, error) { - var result walletParameters + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event FrostWalletRegistrySet failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "walletParameters", - &result, + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, ) - - return result, err } -func (b *Bridge) Wallets( - arg_walletPubKeyHash [20]byte, -) (abi.WalletsWallet, error) { - result, err := b.contract.Wallets( - b.callerOptions, - arg_walletPubKeyHash, +func (b *Bridge) PastFrostWalletRegistrySetEvents( + startBlock uint64, + endBlock *uint64, +) ([]*abi.BridgeFrostWalletRegistrySet, error) { + iterator, err := b.contract.FilterFrostWalletRegistrySet( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, ) - if err != nil { - return result, b.errorResolver.ResolveError( + return nil, fmt.Errorf( + "error retrieving past FrostWalletRegistrySet events: [%v]", err, - b.callerOptions.From, - nil, - "wallets", - arg_walletPubKeyHash, ) } - return result, err -} - -func (b *Bridge) WalletsAtBlock( - arg_walletPubKeyHash [20]byte, - blockNumber *big.Int, -) (abi.WalletsWallet, error) { - var result abi.WalletsWallet + events := make([]*abi.BridgeFrostWalletRegistrySet, 0) - err := chainutil.CallAtBlock( - b.callerOptions.From, - blockNumber, - nil, - b.contractABI, - b.caller, - b.errorResolver, - b.contractAddress, - "wallets", - &result, - arg_walletPubKeyHash, - ) + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } - return result, err + return events, nil } -// ------ Events ------- - -func (b *Bridge) DepositParametersUpdatedEvent( +func (b *Bridge) GovernanceTransferredEvent( opts *ethereum.SubscribeOpts, -) *BDepositParametersUpdatedSubscription { +) *BGovernanceTransferredSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -6564,29 +9438,27 @@ func (b *Bridge) DepositParametersUpdatedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BDepositParametersUpdatedSubscription{ + return &BGovernanceTransferredSubscription{ b, opts, } } -type BDepositParametersUpdatedSubscription struct { +type BGovernanceTransferredSubscription struct { contract *Bridge opts *ethereum.SubscribeOpts } -type bridgeDepositParametersUpdatedFunc func( - DepositDustThreshold uint64, - DepositTreasuryFeeDivisor uint64, - DepositTxMaxFee uint64, - DepositRevealAheadPeriod uint32, +type bridgeGovernanceTransferredFunc func( + OldGovernance common.Address, + NewGovernance common.Address, blockNumber uint64, ) -func (dpus *BDepositParametersUpdatedSubscription) OnEvent( - handler bridgeDepositParametersUpdatedFunc, +func (gts *BGovernanceTransferredSubscription) OnEvent( + handler bridgeGovernanceTransferredFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeDepositParametersUpdated) + eventChan := make(chan *abi.BridgeGovernanceTransferred) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -6596,50 +9468,48 @@ func (dpus *BDepositParametersUpdatedSubscription) OnEvent( return case event := <-eventChan: handler( - event.DepositDustThreshold, - event.DepositTreasuryFeeDivisor, - event.DepositTxMaxFee, - event.DepositRevealAheadPeriod, + event.OldGovernance, + event.NewGovernance, event.Raw.BlockNumber, ) } } }() - sub := dpus.Pipe(eventChan) + sub := gts.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (dpus *BDepositParametersUpdatedSubscription) Pipe( - sink chan *abi.BridgeDepositParametersUpdated, +func (gts *BGovernanceTransferredSubscription) Pipe( + sink chan *abi.BridgeGovernanceTransferred, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(dpus.opts.Tick) + ticker := time.NewTicker(gts.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := dpus.contract.blockCounter.CurrentBlock() + lastBlock, err := gts.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - dpus.opts.PastBlocks + fromBlock := lastBlock - gts.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past DepositParametersUpdated events "+ + "subscription monitoring fetching past GovernanceTransferred events "+ "starting from block [%v]", fromBlock, ) - events, err := dpus.contract.PastDepositParametersUpdatedEvents( + events, err := gts.contract.PastGovernanceTransferredEvents( fromBlock, nil, ) @@ -6651,7 +9521,7 @@ func (dpus *BDepositParametersUpdatedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past DepositParametersUpdated events", + "subscription monitoring fetched [%v] past GovernanceTransferred events", len(events), ) @@ -6662,7 +9532,7 @@ func (dpus *BDepositParametersUpdatedSubscription) Pipe( } }() - sub := dpus.contract.watchDepositParametersUpdated( + sub := gts.contract.watchGovernanceTransferred( sink, ) @@ -6672,11 +9542,11 @@ func (dpus *BDepositParametersUpdatedSubscription) Pipe( }) } -func (b *Bridge) watchDepositParametersUpdated( - sink chan *abi.BridgeDepositParametersUpdated, +func (b *Bridge) watchGovernanceTransferred( + sink chan *abi.BridgeGovernanceTransferred, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchDepositParametersUpdated( + return b.contract.WatchGovernanceTransferred( &bind.WatchOpts{Context: ctx}, sink, ) @@ -6684,7 +9554,7 @@ func (b *Bridge) watchDepositParametersUpdated( thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event DepositParametersUpdated had to be "+ + "subscription to event GovernanceTransferred had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -6693,7 +9563,7 @@ func (b *Bridge) watchDepositParametersUpdated( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event DepositParametersUpdated failed "+ + "subscription to event GovernanceTransferred failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -6709,11 +9579,11 @@ func (b *Bridge) watchDepositParametersUpdated( ) } -func (b *Bridge) PastDepositParametersUpdatedEvents( +func (b *Bridge) PastGovernanceTransferredEvents( startBlock uint64, endBlock *uint64, -) ([]*abi.BridgeDepositParametersUpdated, error) { - iterator, err := b.contract.FilterDepositParametersUpdated( +) ([]*abi.BridgeGovernanceTransferred, error) { + iterator, err := b.contract.FilterGovernanceTransferred( &bind.FilterOpts{ Start: startBlock, End: endBlock, @@ -6721,12 +9591,12 @@ func (b *Bridge) PastDepositParametersUpdatedEvents( ) if err != nil { return nil, fmt.Errorf( - "error retrieving past DepositParametersUpdated events: [%v]", + "error retrieving past GovernanceTransferred events: [%v]", err, ) } - events := make([]*abi.BridgeDepositParametersUpdated, 0) + events := make([]*abi.BridgeGovernanceTransferred, 0) for iterator.Next() { event := iterator.Event @@ -6736,11 +9606,9 @@ func (b *Bridge) PastDepositParametersUpdatedEvents( return events, nil } -func (b *Bridge) DepositRevealedEvent( +func (b *Bridge) InitializedEvent( opts *ethereum.SubscribeOpts, - depositorFilter []common.Address, - walletPubKeyHashFilter [][20]byte, -) *BDepositRevealedSubscription { +) *BInitializedSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -6751,38 +9619,26 @@ func (b *Bridge) DepositRevealedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BDepositRevealedSubscription{ + return &BInitializedSubscription{ b, opts, - depositorFilter, - walletPubKeyHashFilter, } } -type BDepositRevealedSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts - depositorFilter []common.Address - walletPubKeyHashFilter [][20]byte +type BInitializedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts } -type bridgeDepositRevealedFunc func( - FundingTxHash [32]byte, - FundingOutputIndex uint32, - Depositor common.Address, - Amount uint64, - BlindingFactor [8]byte, - WalletPubKeyHash [20]byte, - RefundPubKeyHash [20]byte, - RefundLocktime [4]byte, - Vault common.Address, +type bridgeInitializedFunc func( + Version uint8, blockNumber uint64, ) -func (drs *BDepositRevealedSubscription) OnEvent( - handler bridgeDepositRevealedFunc, +func (is *BInitializedSubscription) OnEvent( + handler bridgeInitializedFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeDepositRevealed) + eventChan := make(chan *abi.BridgeInitialized) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -6792,59 +9648,49 @@ func (drs *BDepositRevealedSubscription) OnEvent( return case event := <-eventChan: handler( - event.FundingTxHash, - event.FundingOutputIndex, - event.Depositor, - event.Amount, - event.BlindingFactor, - event.WalletPubKeyHash, - event.RefundPubKeyHash, - event.RefundLocktime, - event.Vault, + event.Version, event.Raw.BlockNumber, ) } } }() - sub := drs.Pipe(eventChan) + sub := is.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (drs *BDepositRevealedSubscription) Pipe( - sink chan *abi.BridgeDepositRevealed, +func (is *BInitializedSubscription) Pipe( + sink chan *abi.BridgeInitialized, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(drs.opts.Tick) + ticker := time.NewTicker(is.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := drs.contract.blockCounter.CurrentBlock() + lastBlock, err := is.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - drs.opts.PastBlocks + fromBlock := lastBlock - is.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past DepositRevealed events "+ + "subscription monitoring fetching past Initialized events "+ "starting from block [%v]", fromBlock, ) - events, err := drs.contract.PastDepositRevealedEvents( + events, err := is.contract.PastInitializedEvents( fromBlock, nil, - drs.depositorFilter, - drs.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -6854,7 +9700,7 @@ func (drs *BDepositRevealedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past DepositRevealed events", + "subscription monitoring fetched [%v] past Initialized events", len(events), ) @@ -6865,10 +9711,8 @@ func (drs *BDepositRevealedSubscription) Pipe( } }() - sub := drs.contract.watchDepositRevealed( + sub := is.contract.watchInitialized( sink, - drs.depositorFilter, - drs.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -6877,23 +9721,19 @@ func (drs *BDepositRevealedSubscription) Pipe( }) } -func (b *Bridge) watchDepositRevealed( - sink chan *abi.BridgeDepositRevealed, - depositorFilter []common.Address, - walletPubKeyHashFilter [][20]byte, +func (b *Bridge) watchInitialized( + sink chan *abi.BridgeInitialized, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchDepositRevealed( + return b.contract.WatchInitialized( &bind.WatchOpts{Context: ctx}, sink, - depositorFilter, - walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event DepositRevealed had to be "+ + "subscription to event Initialized had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -6902,7 +9742,7 @@ func (b *Bridge) watchDepositRevealed( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event DepositRevealed failed "+ + "subscription to event Initialized failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -6918,28 +9758,24 @@ func (b *Bridge) watchDepositRevealed( ) } -func (b *Bridge) PastDepositRevealedEvents( +func (b *Bridge) PastInitializedEvents( startBlock uint64, endBlock *uint64, - depositorFilter []common.Address, - walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeDepositRevealed, error) { - iterator, err := b.contract.FilterDepositRevealed( +) ([]*abi.BridgeInitialized, error) { + iterator, err := b.contract.FilterInitialized( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, - depositorFilter, - walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past DepositRevealed events: [%v]", + "error retrieving past Initialized events: [%v]", err, ) } - events := make([]*abi.BridgeDepositRevealed, 0) + events := make([]*abi.BridgeInitialized, 0) for iterator.Next() { event := iterator.Event @@ -6949,9 +9785,12 @@ func (b *Bridge) PastDepositRevealedEvents( return events, nil } -func (b *Bridge) DepositsSweptEvent( +func (b *Bridge) LegacyFraudChallengeMigratedEvent( opts *ethereum.SubscribeOpts, -) *BDepositsSweptSubscription { + routerKindFilter []uint8, + challengeKeyFilter []*big.Int, + challengerFilter []common.Address, +) *BLegacyFraudChallengeMigratedSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -6962,27 +9801,35 @@ func (b *Bridge) DepositsSweptEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BDepositsSweptSubscription{ + return &BLegacyFraudChallengeMigratedSubscription{ b, opts, + routerKindFilter, + challengeKeyFilter, + challengerFilter, } } -type BDepositsSweptSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts +type BLegacyFraudChallengeMigratedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + routerKindFilter []uint8 + challengeKeyFilter []*big.Int + challengerFilter []common.Address } -type bridgeDepositsSweptFunc func( - WalletPubKeyHash [20]byte, - SweepTxHash [32]byte, +type bridgeLegacyFraudChallengeMigratedFunc func( + RouterKind uint8, + ChallengeKey *big.Int, + Challenger common.Address, + DepositAmount *big.Int, blockNumber uint64, ) -func (dss *BDepositsSweptSubscription) OnEvent( - handler bridgeDepositsSweptFunc, +func (lfcms *BLegacyFraudChallengeMigratedSubscription) OnEvent( + handler bridgeLegacyFraudChallengeMigratedFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeDepositsSwept) + eventChan := make(chan *abi.BridgeLegacyFraudChallengeMigrated) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -6992,50 +9839,55 @@ func (dss *BDepositsSweptSubscription) OnEvent( return case event := <-eventChan: handler( - event.WalletPubKeyHash, - event.SweepTxHash, + event.RouterKind, + event.ChallengeKey, + event.Challenger, + event.DepositAmount, event.Raw.BlockNumber, ) } } }() - sub := dss.Pipe(eventChan) + sub := lfcms.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (dss *BDepositsSweptSubscription) Pipe( - sink chan *abi.BridgeDepositsSwept, +func (lfcms *BLegacyFraudChallengeMigratedSubscription) Pipe( + sink chan *abi.BridgeLegacyFraudChallengeMigrated, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(dss.opts.Tick) + ticker := time.NewTicker(lfcms.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := dss.contract.blockCounter.CurrentBlock() + lastBlock, err := lfcms.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - dss.opts.PastBlocks + fromBlock := lastBlock - lfcms.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past DepositsSwept events "+ + "subscription monitoring fetching past LegacyFraudChallengeMigrated events "+ "starting from block [%v]", fromBlock, ) - events, err := dss.contract.PastDepositsSweptEvents( + events, err := lfcms.contract.PastLegacyFraudChallengeMigratedEvents( fromBlock, nil, + lfcms.routerKindFilter, + lfcms.challengeKeyFilter, + lfcms.challengerFilter, ) if err != nil { bLogger.Errorf( @@ -7045,7 +9897,7 @@ func (dss *BDepositsSweptSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past DepositsSwept events", + "subscription monitoring fetched [%v] past LegacyFraudChallengeMigrated events", len(events), ) @@ -7056,8 +9908,11 @@ func (dss *BDepositsSweptSubscription) Pipe( } }() - sub := dss.contract.watchDepositsSwept( + sub := lfcms.contract.watchLegacyFraudChallengeMigrated( sink, + lfcms.routerKindFilter, + lfcms.challengeKeyFilter, + lfcms.challengerFilter, ) return subscription.NewEventSubscription(func() { @@ -7066,19 +9921,25 @@ func (dss *BDepositsSweptSubscription) Pipe( }) } -func (b *Bridge) watchDepositsSwept( - sink chan *abi.BridgeDepositsSwept, +func (b *Bridge) watchLegacyFraudChallengeMigrated( + sink chan *abi.BridgeLegacyFraudChallengeMigrated, + routerKindFilter []uint8, + challengeKeyFilter []*big.Int, + challengerFilter []common.Address, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchDepositsSwept( + return b.contract.WatchLegacyFraudChallengeMigrated( &bind.WatchOpts{Context: ctx}, sink, + routerKindFilter, + challengeKeyFilter, + challengerFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event DepositsSwept had to be "+ + "subscription to event LegacyFraudChallengeMigrated had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -7087,7 +9948,7 @@ func (b *Bridge) watchDepositsSwept( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event DepositsSwept failed "+ + "subscription to event LegacyFraudChallengeMigrated failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -7103,24 +9964,30 @@ func (b *Bridge) watchDepositsSwept( ) } -func (b *Bridge) PastDepositsSweptEvents( +func (b *Bridge) PastLegacyFraudChallengeMigratedEvents( startBlock uint64, endBlock *uint64, -) ([]*abi.BridgeDepositsSwept, error) { - iterator, err := b.contract.FilterDepositsSwept( + routerKindFilter []uint8, + challengeKeyFilter []*big.Int, + challengerFilter []common.Address, +) ([]*abi.BridgeLegacyFraudChallengeMigrated, error) { + iterator, err := b.contract.FilterLegacyFraudChallengeMigrated( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, + routerKindFilter, + challengeKeyFilter, + challengerFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past DepositsSwept events: [%v]", + "error retrieving past LegacyFraudChallengeMigrated events: [%v]", err, ) } - events := make([]*abi.BridgeDepositsSwept, 0) + events := make([]*abi.BridgeLegacyFraudChallengeMigrated, 0) for iterator.Next() { event := iterator.Event @@ -7130,10 +9997,9 @@ func (b *Bridge) PastDepositsSweptEvents( return events, nil } -func (b *Bridge) FraudChallengeDefeatTimedOutEvent( +func (b *Bridge) LifecycleRouterSetEvent( opts *ethereum.SubscribeOpts, - walletPubKeyHashFilter [][20]byte, -) *BFraudChallengeDefeatTimedOutSubscription { +) *BLifecycleRouterSetSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -7144,29 +10010,26 @@ func (b *Bridge) FraudChallengeDefeatTimedOutEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BFraudChallengeDefeatTimedOutSubscription{ + return &BLifecycleRouterSetSubscription{ b, opts, - walletPubKeyHashFilter, } } -type BFraudChallengeDefeatTimedOutSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts - walletPubKeyHashFilter [][20]byte +type BLifecycleRouterSetSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts } -type bridgeFraudChallengeDefeatTimedOutFunc func( - WalletPubKeyHash [20]byte, - Sighash [32]byte, +type bridgeLifecycleRouterSetFunc func( + LifecycleRouter common.Address, blockNumber uint64, ) -func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) OnEvent( - handler bridgeFraudChallengeDefeatTimedOutFunc, +func (lrss *BLifecycleRouterSetSubscription) OnEvent( + handler bridgeLifecycleRouterSetFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeFraudChallengeDefeatTimedOut) + eventChan := make(chan *abi.BridgeLifecycleRouterSet) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -7176,51 +10039,49 @@ func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) OnEvent( return case event := <-eventChan: handler( - event.WalletPubKeyHash, - event.Sighash, + event.LifecycleRouter, event.Raw.BlockNumber, ) } } }() - sub := fcdtos.Pipe(eventChan) + sub := lrss.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) Pipe( - sink chan *abi.BridgeFraudChallengeDefeatTimedOut, +func (lrss *BLifecycleRouterSetSubscription) Pipe( + sink chan *abi.BridgeLifecycleRouterSet, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(fcdtos.opts.Tick) + ticker := time.NewTicker(lrss.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := fcdtos.contract.blockCounter.CurrentBlock() + lastBlock, err := lrss.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - fcdtos.opts.PastBlocks + fromBlock := lastBlock - lrss.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past FraudChallengeDefeatTimedOut events "+ + "subscription monitoring fetching past LifecycleRouterSet events "+ "starting from block [%v]", fromBlock, ) - events, err := fcdtos.contract.PastFraudChallengeDefeatTimedOutEvents( + events, err := lrss.contract.PastLifecycleRouterSetEvents( fromBlock, nil, - fcdtos.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -7230,7 +10091,7 @@ func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past FraudChallengeDefeatTimedOut events", + "subscription monitoring fetched [%v] past LifecycleRouterSet events", len(events), ) @@ -7241,9 +10102,8 @@ func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) Pipe( } }() - sub := fcdtos.contract.watchFraudChallengeDefeatTimedOut( + sub := lrss.contract.watchLifecycleRouterSet( sink, - fcdtos.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -7252,21 +10112,19 @@ func (fcdtos *BFraudChallengeDefeatTimedOutSubscription) Pipe( }) } -func (b *Bridge) watchFraudChallengeDefeatTimedOut( - sink chan *abi.BridgeFraudChallengeDefeatTimedOut, - walletPubKeyHashFilter [][20]byte, +func (b *Bridge) watchLifecycleRouterSet( + sink chan *abi.BridgeLifecycleRouterSet, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchFraudChallengeDefeatTimedOut( + return b.contract.WatchLifecycleRouterSet( &bind.WatchOpts{Context: ctx}, sink, - walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event FraudChallengeDefeatTimedOut had to be "+ + "subscription to event LifecycleRouterSet had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -7275,7 +10133,7 @@ func (b *Bridge) watchFraudChallengeDefeatTimedOut( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event FraudChallengeDefeatTimedOut failed "+ + "subscription to event LifecycleRouterSet failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -7291,26 +10149,24 @@ func (b *Bridge) watchFraudChallengeDefeatTimedOut( ) } -func (b *Bridge) PastFraudChallengeDefeatTimedOutEvents( +func (b *Bridge) PastLifecycleRouterSetEvents( startBlock uint64, endBlock *uint64, - walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeFraudChallengeDefeatTimedOut, error) { - iterator, err := b.contract.FilterFraudChallengeDefeatTimedOut( +) ([]*abi.BridgeLifecycleRouterSet, error) { + iterator, err := b.contract.FilterLifecycleRouterSet( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, - walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past FraudChallengeDefeatTimedOut events: [%v]", + "error retrieving past LifecycleRouterSet events: [%v]", err, ) } - events := make([]*abi.BridgeFraudChallengeDefeatTimedOut, 0) + events := make([]*abi.BridgeLifecycleRouterSet, 0) for iterator.Next() { event := iterator.Event @@ -7320,10 +10176,10 @@ func (b *Bridge) PastFraudChallengeDefeatTimedOutEvents( return events, nil } -func (b *Bridge) FraudChallengeDefeatedEvent( +func (b *Bridge) MovedFundsSweepTimedOutEvent( opts *ethereum.SubscribeOpts, walletPubKeyHashFilter [][20]byte, -) *BFraudChallengeDefeatedSubscription { +) *BMovedFundsSweepTimedOutSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -7334,29 +10190,30 @@ func (b *Bridge) FraudChallengeDefeatedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BFraudChallengeDefeatedSubscription{ + return &BMovedFundsSweepTimedOutSubscription{ b, opts, walletPubKeyHashFilter, } } -type BFraudChallengeDefeatedSubscription struct { +type BMovedFundsSweepTimedOutSubscription struct { contract *Bridge opts *ethereum.SubscribeOpts walletPubKeyHashFilter [][20]byte } -type bridgeFraudChallengeDefeatedFunc func( +type bridgeMovedFundsSweepTimedOutFunc func( WalletPubKeyHash [20]byte, - Sighash [32]byte, + MovingFundsTxHash [32]byte, + MovingFundsTxOutputIndex uint32, blockNumber uint64, ) -func (fcds *BFraudChallengeDefeatedSubscription) OnEvent( - handler bridgeFraudChallengeDefeatedFunc, +func (mfstos *BMovedFundsSweepTimedOutSubscription) OnEvent( + handler bridgeMovedFundsSweepTimedOutFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeFraudChallengeDefeated) + eventChan := make(chan *abi.BridgeMovedFundsSweepTimedOut) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -7367,50 +10224,51 @@ func (fcds *BFraudChallengeDefeatedSubscription) OnEvent( case event := <-eventChan: handler( event.WalletPubKeyHash, - event.Sighash, + event.MovingFundsTxHash, + event.MovingFundsTxOutputIndex, event.Raw.BlockNumber, ) } } }() - sub := fcds.Pipe(eventChan) + sub := mfstos.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (fcds *BFraudChallengeDefeatedSubscription) Pipe( - sink chan *abi.BridgeFraudChallengeDefeated, +func (mfstos *BMovedFundsSweepTimedOutSubscription) Pipe( + sink chan *abi.BridgeMovedFundsSweepTimedOut, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(fcds.opts.Tick) + ticker := time.NewTicker(mfstos.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := fcds.contract.blockCounter.CurrentBlock() + lastBlock, err := mfstos.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - fcds.opts.PastBlocks + fromBlock := lastBlock - mfstos.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past FraudChallengeDefeated events "+ + "subscription monitoring fetching past MovedFundsSweepTimedOut events "+ "starting from block [%v]", fromBlock, ) - events, err := fcds.contract.PastFraudChallengeDefeatedEvents( + events, err := mfstos.contract.PastMovedFundsSweepTimedOutEvents( fromBlock, nil, - fcds.walletPubKeyHashFilter, + mfstos.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -7420,7 +10278,7 @@ func (fcds *BFraudChallengeDefeatedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past FraudChallengeDefeated events", + "subscription monitoring fetched [%v] past MovedFundsSweepTimedOut events", len(events), ) @@ -7431,9 +10289,9 @@ func (fcds *BFraudChallengeDefeatedSubscription) Pipe( } }() - sub := fcds.contract.watchFraudChallengeDefeated( + sub := mfstos.contract.watchMovedFundsSweepTimedOut( sink, - fcds.walletPubKeyHashFilter, + mfstos.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -7442,12 +10300,12 @@ func (fcds *BFraudChallengeDefeatedSubscription) Pipe( }) } -func (b *Bridge) watchFraudChallengeDefeated( - sink chan *abi.BridgeFraudChallengeDefeated, +func (b *Bridge) watchMovedFundsSweepTimedOut( + sink chan *abi.BridgeMovedFundsSweepTimedOut, walletPubKeyHashFilter [][20]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchFraudChallengeDefeated( + return b.contract.WatchMovedFundsSweepTimedOut( &bind.WatchOpts{Context: ctx}, sink, walletPubKeyHashFilter, @@ -7456,7 +10314,7 @@ func (b *Bridge) watchFraudChallengeDefeated( thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event FraudChallengeDefeated had to be "+ + "subscription to event MovedFundsSweepTimedOut had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -7465,7 +10323,7 @@ func (b *Bridge) watchFraudChallengeDefeated( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event FraudChallengeDefeated failed "+ + "subscription to event MovedFundsSweepTimedOut failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -7481,12 +10339,12 @@ func (b *Bridge) watchFraudChallengeDefeated( ) } -func (b *Bridge) PastFraudChallengeDefeatedEvents( +func (b *Bridge) PastMovedFundsSweepTimedOutEvents( startBlock uint64, endBlock *uint64, walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeFraudChallengeDefeated, error) { - iterator, err := b.contract.FilterFraudChallengeDefeated( +) ([]*abi.BridgeMovedFundsSweepTimedOut, error) { + iterator, err := b.contract.FilterMovedFundsSweepTimedOut( &bind.FilterOpts{ Start: startBlock, End: endBlock, @@ -7495,12 +10353,12 @@ func (b *Bridge) PastFraudChallengeDefeatedEvents( ) if err != nil { return nil, fmt.Errorf( - "error retrieving past FraudChallengeDefeated events: [%v]", + "error retrieving past MovedFundsSweepTimedOut events: [%v]", err, ) } - events := make([]*abi.BridgeFraudChallengeDefeated, 0) + events := make([]*abi.BridgeMovedFundsSweepTimedOut, 0) for iterator.Next() { event := iterator.Event @@ -7510,10 +10368,10 @@ func (b *Bridge) PastFraudChallengeDefeatedEvents( return events, nil } -func (b *Bridge) FraudChallengeSubmittedEvent( +func (b *Bridge) MovedFundsSweptEvent( opts *ethereum.SubscribeOpts, walletPubKeyHashFilter [][20]byte, -) *BFraudChallengeSubmittedSubscription { +) *BMovedFundsSweptSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -7524,32 +10382,29 @@ func (b *Bridge) FraudChallengeSubmittedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BFraudChallengeSubmittedSubscription{ + return &BMovedFundsSweptSubscription{ b, opts, walletPubKeyHashFilter, } } -type BFraudChallengeSubmittedSubscription struct { +type BMovedFundsSweptSubscription struct { contract *Bridge opts *ethereum.SubscribeOpts walletPubKeyHashFilter [][20]byte } -type bridgeFraudChallengeSubmittedFunc func( +type bridgeMovedFundsSweptFunc func( WalletPubKeyHash [20]byte, - Sighash [32]byte, - V uint8, - R [32]byte, - S [32]byte, + SweepTxHash [32]byte, blockNumber uint64, ) -func (fcss *BFraudChallengeSubmittedSubscription) OnEvent( - handler bridgeFraudChallengeSubmittedFunc, +func (mfss *BMovedFundsSweptSubscription) OnEvent( + handler bridgeMovedFundsSweptFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeFraudChallengeSubmitted) + eventChan := make(chan *abi.BridgeMovedFundsSwept) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -7560,53 +10415,50 @@ func (fcss *BFraudChallengeSubmittedSubscription) OnEvent( case event := <-eventChan: handler( event.WalletPubKeyHash, - event.Sighash, - event.V, - event.R, - event.S, + event.SweepTxHash, event.Raw.BlockNumber, ) } } }() - sub := fcss.Pipe(eventChan) + sub := mfss.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (fcss *BFraudChallengeSubmittedSubscription) Pipe( - sink chan *abi.BridgeFraudChallengeSubmitted, +func (mfss *BMovedFundsSweptSubscription) Pipe( + sink chan *abi.BridgeMovedFundsSwept, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(fcss.opts.Tick) + ticker := time.NewTicker(mfss.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := fcss.contract.blockCounter.CurrentBlock() + lastBlock, err := mfss.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - fcss.opts.PastBlocks + fromBlock := lastBlock - mfss.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past FraudChallengeSubmitted events "+ + "subscription monitoring fetching past MovedFundsSwept events "+ "starting from block [%v]", fromBlock, ) - events, err := fcss.contract.PastFraudChallengeSubmittedEvents( + events, err := mfss.contract.PastMovedFundsSweptEvents( fromBlock, nil, - fcss.walletPubKeyHashFilter, + mfss.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -7616,7 +10468,7 @@ func (fcss *BFraudChallengeSubmittedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past FraudChallengeSubmitted events", + "subscription monitoring fetched [%v] past MovedFundsSwept events", len(events), ) @@ -7627,9 +10479,9 @@ func (fcss *BFraudChallengeSubmittedSubscription) Pipe( } }() - sub := fcss.contract.watchFraudChallengeSubmitted( + sub := mfss.contract.watchMovedFundsSwept( sink, - fcss.walletPubKeyHashFilter, + mfss.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -7638,12 +10490,12 @@ func (fcss *BFraudChallengeSubmittedSubscription) Pipe( }) } -func (b *Bridge) watchFraudChallengeSubmitted( - sink chan *abi.BridgeFraudChallengeSubmitted, +func (b *Bridge) watchMovedFundsSwept( + sink chan *abi.BridgeMovedFundsSwept, walletPubKeyHashFilter [][20]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchFraudChallengeSubmitted( + return b.contract.WatchMovedFundsSwept( &bind.WatchOpts{Context: ctx}, sink, walletPubKeyHashFilter, @@ -7652,7 +10504,7 @@ func (b *Bridge) watchFraudChallengeSubmitted( thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event FraudChallengeSubmitted had to be "+ + "subscription to event MovedFundsSwept had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -7661,7 +10513,7 @@ func (b *Bridge) watchFraudChallengeSubmitted( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event FraudChallengeSubmitted failed "+ + "subscription to event MovedFundsSwept failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -7677,12 +10529,12 @@ func (b *Bridge) watchFraudChallengeSubmitted( ) } -func (b *Bridge) PastFraudChallengeSubmittedEvents( +func (b *Bridge) PastMovedFundsSweptEvents( startBlock uint64, endBlock *uint64, walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeFraudChallengeSubmitted, error) { - iterator, err := b.contract.FilterFraudChallengeSubmitted( +) ([]*abi.BridgeMovedFundsSwept, error) { + iterator, err := b.contract.FilterMovedFundsSwept( &bind.FilterOpts{ Start: startBlock, End: endBlock, @@ -7691,12 +10543,12 @@ func (b *Bridge) PastFraudChallengeSubmittedEvents( ) if err != nil { return nil, fmt.Errorf( - "error retrieving past FraudChallengeSubmitted events: [%v]", + "error retrieving past MovedFundsSwept events: [%v]", err, ) } - events := make([]*abi.BridgeFraudChallengeSubmitted, 0) + events := make([]*abi.BridgeMovedFundsSwept, 0) for iterator.Next() { event := iterator.Event @@ -7706,9 +10558,10 @@ func (b *Bridge) PastFraudChallengeSubmittedEvents( return events, nil } -func (b *Bridge) FraudParametersUpdatedEvent( +func (b *Bridge) MovingFundsBelowDustReportedEvent( opts *ethereum.SubscribeOpts, -) *BFraudParametersUpdatedSubscription { + walletPubKeyHashFilter [][20]byte, +) *BMovingFundsBelowDustReportedSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -7719,29 +10572,28 @@ func (b *Bridge) FraudParametersUpdatedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BFraudParametersUpdatedSubscription{ + return &BMovingFundsBelowDustReportedSubscription{ b, opts, + walletPubKeyHashFilter, } } -type BFraudParametersUpdatedSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts +type BMovingFundsBelowDustReportedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + walletPubKeyHashFilter [][20]byte } -type bridgeFraudParametersUpdatedFunc func( - FraudChallengeDepositAmount *big.Int, - FraudChallengeDefeatTimeout uint32, - FraudSlashingAmount *big.Int, - FraudNotifierRewardMultiplier uint32, +type bridgeMovingFundsBelowDustReportedFunc func( + WalletPubKeyHash [20]byte, blockNumber uint64, ) -func (fpus *BFraudParametersUpdatedSubscription) OnEvent( - handler bridgeFraudParametersUpdatedFunc, +func (mfbdrs *BMovingFundsBelowDustReportedSubscription) OnEvent( + handler bridgeMovingFundsBelowDustReportedFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeFraudParametersUpdated) + eventChan := make(chan *abi.BridgeMovingFundsBelowDustReported) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -7751,52 +10603,50 @@ func (fpus *BFraudParametersUpdatedSubscription) OnEvent( return case event := <-eventChan: handler( - event.FraudChallengeDepositAmount, - event.FraudChallengeDefeatTimeout, - event.FraudSlashingAmount, - event.FraudNotifierRewardMultiplier, + event.WalletPubKeyHash, event.Raw.BlockNumber, ) } } }() - sub := fpus.Pipe(eventChan) + sub := mfbdrs.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (fpus *BFraudParametersUpdatedSubscription) Pipe( - sink chan *abi.BridgeFraudParametersUpdated, +func (mfbdrs *BMovingFundsBelowDustReportedSubscription) Pipe( + sink chan *abi.BridgeMovingFundsBelowDustReported, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(fpus.opts.Tick) + ticker := time.NewTicker(mfbdrs.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := fpus.contract.blockCounter.CurrentBlock() + lastBlock, err := mfbdrs.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - fpus.opts.PastBlocks + fromBlock := lastBlock - mfbdrs.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past FraudParametersUpdated events "+ + "subscription monitoring fetching past MovingFundsBelowDustReported events "+ "starting from block [%v]", fromBlock, ) - events, err := fpus.contract.PastFraudParametersUpdatedEvents( + events, err := mfbdrs.contract.PastMovingFundsBelowDustReportedEvents( fromBlock, nil, + mfbdrs.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -7806,7 +10656,7 @@ func (fpus *BFraudParametersUpdatedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past FraudParametersUpdated events", + "subscription monitoring fetched [%v] past MovingFundsBelowDustReported events", len(events), ) @@ -7817,8 +10667,9 @@ func (fpus *BFraudParametersUpdatedSubscription) Pipe( } }() - sub := fpus.contract.watchFraudParametersUpdated( + sub := mfbdrs.contract.watchMovingFundsBelowDustReported( sink, + mfbdrs.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -7827,19 +10678,21 @@ func (fpus *BFraudParametersUpdatedSubscription) Pipe( }) } -func (b *Bridge) watchFraudParametersUpdated( - sink chan *abi.BridgeFraudParametersUpdated, +func (b *Bridge) watchMovingFundsBelowDustReported( + sink chan *abi.BridgeMovingFundsBelowDustReported, + walletPubKeyHashFilter [][20]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchFraudParametersUpdated( + return b.contract.WatchMovingFundsBelowDustReported( &bind.WatchOpts{Context: ctx}, sink, + walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event FraudParametersUpdated had to be "+ + "subscription to event MovingFundsBelowDustReported had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -7848,7 +10701,7 @@ func (b *Bridge) watchFraudParametersUpdated( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event FraudParametersUpdated failed "+ + "subscription to event MovingFundsBelowDustReported failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -7864,24 +10717,26 @@ func (b *Bridge) watchFraudParametersUpdated( ) } -func (b *Bridge) PastFraudParametersUpdatedEvents( +func (b *Bridge) PastMovingFundsBelowDustReportedEvents( startBlock uint64, endBlock *uint64, -) ([]*abi.BridgeFraudParametersUpdated, error) { - iterator, err := b.contract.FilterFraudParametersUpdated( + walletPubKeyHashFilter [][20]byte, +) ([]*abi.BridgeMovingFundsBelowDustReported, error) { + iterator, err := b.contract.FilterMovingFundsBelowDustReported( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, + walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past FraudParametersUpdated events: [%v]", + "error retrieving past MovingFundsBelowDustReported events: [%v]", err, ) } - events := make([]*abi.BridgeFraudParametersUpdated, 0) + events := make([]*abi.BridgeMovingFundsBelowDustReported, 0) for iterator.Next() { event := iterator.Event @@ -7891,9 +10746,10 @@ func (b *Bridge) PastFraudParametersUpdatedEvents( return events, nil } -func (b *Bridge) GovernanceTransferredEvent( +func (b *Bridge) MovingFundsCommitmentSubmittedEvent( opts *ethereum.SubscribeOpts, -) *BGovernanceTransferredSubscription { + walletPubKeyHashFilter [][20]byte, +) *BMovingFundsCommitmentSubmittedSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -7904,27 +10760,30 @@ func (b *Bridge) GovernanceTransferredEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BGovernanceTransferredSubscription{ + return &BMovingFundsCommitmentSubmittedSubscription{ b, opts, + walletPubKeyHashFilter, } } -type BGovernanceTransferredSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts +type BMovingFundsCommitmentSubmittedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + walletPubKeyHashFilter [][20]byte } -type bridgeGovernanceTransferredFunc func( - OldGovernance common.Address, - NewGovernance common.Address, +type bridgeMovingFundsCommitmentSubmittedFunc func( + WalletPubKeyHash [20]byte, + TargetWallets [][20]byte, + Submitter common.Address, blockNumber uint64, ) -func (gts *BGovernanceTransferredSubscription) OnEvent( - handler bridgeGovernanceTransferredFunc, +func (mfcss *BMovingFundsCommitmentSubmittedSubscription) OnEvent( + handler bridgeMovingFundsCommitmentSubmittedFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeGovernanceTransferred) + eventChan := make(chan *abi.BridgeMovingFundsCommitmentSubmitted) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -7934,50 +10793,52 @@ func (gts *BGovernanceTransferredSubscription) OnEvent( return case event := <-eventChan: handler( - event.OldGovernance, - event.NewGovernance, + event.WalletPubKeyHash, + event.TargetWallets, + event.Submitter, event.Raw.BlockNumber, ) } } }() - sub := gts.Pipe(eventChan) + sub := mfcss.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (gts *BGovernanceTransferredSubscription) Pipe( - sink chan *abi.BridgeGovernanceTransferred, +func (mfcss *BMovingFundsCommitmentSubmittedSubscription) Pipe( + sink chan *abi.BridgeMovingFundsCommitmentSubmitted, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(gts.opts.Tick) + ticker := time.NewTicker(mfcss.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := gts.contract.blockCounter.CurrentBlock() + lastBlock, err := mfcss.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - gts.opts.PastBlocks + fromBlock := lastBlock - mfcss.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past GovernanceTransferred events "+ + "subscription monitoring fetching past MovingFundsCommitmentSubmitted events "+ "starting from block [%v]", fromBlock, ) - events, err := gts.contract.PastGovernanceTransferredEvents( + events, err := mfcss.contract.PastMovingFundsCommitmentSubmittedEvents( fromBlock, nil, + mfcss.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -7987,7 +10848,7 @@ func (gts *BGovernanceTransferredSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past GovernanceTransferred events", + "subscription monitoring fetched [%v] past MovingFundsCommitmentSubmitted events", len(events), ) @@ -7998,8 +10859,9 @@ func (gts *BGovernanceTransferredSubscription) Pipe( } }() - sub := gts.contract.watchGovernanceTransferred( + sub := mfcss.contract.watchMovingFundsCommitmentSubmitted( sink, + mfcss.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -8008,19 +10870,21 @@ func (gts *BGovernanceTransferredSubscription) Pipe( }) } -func (b *Bridge) watchGovernanceTransferred( - sink chan *abi.BridgeGovernanceTransferred, +func (b *Bridge) watchMovingFundsCommitmentSubmitted( + sink chan *abi.BridgeMovingFundsCommitmentSubmitted, + walletPubKeyHashFilter [][20]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchGovernanceTransferred( + return b.contract.WatchMovingFundsCommitmentSubmitted( &bind.WatchOpts{Context: ctx}, sink, + walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event GovernanceTransferred had to be "+ + "subscription to event MovingFundsCommitmentSubmitted had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -8029,7 +10893,7 @@ func (b *Bridge) watchGovernanceTransferred( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event GovernanceTransferred failed "+ + "subscription to event MovingFundsCommitmentSubmitted failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -8045,24 +10909,26 @@ func (b *Bridge) watchGovernanceTransferred( ) } -func (b *Bridge) PastGovernanceTransferredEvents( +func (b *Bridge) PastMovingFundsCommitmentSubmittedEvents( startBlock uint64, endBlock *uint64, -) ([]*abi.BridgeGovernanceTransferred, error) { - iterator, err := b.contract.FilterGovernanceTransferred( + walletPubKeyHashFilter [][20]byte, +) ([]*abi.BridgeMovingFundsCommitmentSubmitted, error) { + iterator, err := b.contract.FilterMovingFundsCommitmentSubmitted( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, + walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past GovernanceTransferred events: [%v]", + "error retrieving past MovingFundsCommitmentSubmitted events: [%v]", err, ) } - events := make([]*abi.BridgeGovernanceTransferred, 0) + events := make([]*abi.BridgeMovingFundsCommitmentSubmitted, 0) for iterator.Next() { event := iterator.Event @@ -8072,9 +10938,10 @@ func (b *Bridge) PastGovernanceTransferredEvents( return events, nil } -func (b *Bridge) InitializedEvent( +func (b *Bridge) MovingFundsCompletedEvent( opts *ethereum.SubscribeOpts, -) *BInitializedSubscription { + walletPubKeyHashFilter [][20]byte, +) *BMovingFundsCompletedSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -8085,26 +10952,29 @@ func (b *Bridge) InitializedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BInitializedSubscription{ + return &BMovingFundsCompletedSubscription{ b, opts, + walletPubKeyHashFilter, } } -type BInitializedSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts +type BMovingFundsCompletedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + walletPubKeyHashFilter [][20]byte } -type bridgeInitializedFunc func( - Version uint8, +type bridgeMovingFundsCompletedFunc func( + WalletPubKeyHash [20]byte, + MovingFundsTxHash [32]byte, blockNumber uint64, ) -func (is *BInitializedSubscription) OnEvent( - handler bridgeInitializedFunc, +func (mfcs *BMovingFundsCompletedSubscription) OnEvent( + handler bridgeMovingFundsCompletedFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeInitialized) + eventChan := make(chan *abi.BridgeMovingFundsCompleted) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -8114,49 +10984,51 @@ func (is *BInitializedSubscription) OnEvent( return case event := <-eventChan: handler( - event.Version, + event.WalletPubKeyHash, + event.MovingFundsTxHash, event.Raw.BlockNumber, ) } } }() - sub := is.Pipe(eventChan) + sub := mfcs.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (is *BInitializedSubscription) Pipe( - sink chan *abi.BridgeInitialized, +func (mfcs *BMovingFundsCompletedSubscription) Pipe( + sink chan *abi.BridgeMovingFundsCompleted, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(is.opts.Tick) + ticker := time.NewTicker(mfcs.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := is.contract.blockCounter.CurrentBlock() + lastBlock, err := mfcs.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - is.opts.PastBlocks + fromBlock := lastBlock - mfcs.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past Initialized events "+ + "subscription monitoring fetching past MovingFundsCompleted events "+ "starting from block [%v]", fromBlock, ) - events, err := is.contract.PastInitializedEvents( + events, err := mfcs.contract.PastMovingFundsCompletedEvents( fromBlock, nil, + mfcs.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -8166,7 +11038,7 @@ func (is *BInitializedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past Initialized events", + "subscription monitoring fetched [%v] past MovingFundsCompleted events", len(events), ) @@ -8177,8 +11049,9 @@ func (is *BInitializedSubscription) Pipe( } }() - sub := is.contract.watchInitialized( + sub := mfcs.contract.watchMovingFundsCompleted( sink, + mfcs.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -8187,19 +11060,21 @@ func (is *BInitializedSubscription) Pipe( }) } -func (b *Bridge) watchInitialized( - sink chan *abi.BridgeInitialized, +func (b *Bridge) watchMovingFundsCompleted( + sink chan *abi.BridgeMovingFundsCompleted, + walletPubKeyHashFilter [][20]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchInitialized( + return b.contract.WatchMovingFundsCompleted( &bind.WatchOpts{Context: ctx}, sink, + walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event Initialized had to be "+ + "subscription to event MovingFundsCompleted had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -8208,7 +11083,7 @@ func (b *Bridge) watchInitialized( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event Initialized failed "+ + "subscription to event MovingFundsCompleted failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -8224,24 +11099,26 @@ func (b *Bridge) watchInitialized( ) } -func (b *Bridge) PastInitializedEvents( +func (b *Bridge) PastMovingFundsCompletedEvents( startBlock uint64, endBlock *uint64, -) ([]*abi.BridgeInitialized, error) { - iterator, err := b.contract.FilterInitialized( + walletPubKeyHashFilter [][20]byte, +) ([]*abi.BridgeMovingFundsCompleted, error) { + iterator, err := b.contract.FilterMovingFundsCompleted( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, + walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past Initialized events: [%v]", + "error retrieving past MovingFundsCompleted events: [%v]", err, ) } - events := make([]*abi.BridgeInitialized, 0) + events := make([]*abi.BridgeMovingFundsCompleted, 0) for iterator.Next() { event := iterator.Event @@ -8251,10 +11128,9 @@ func (b *Bridge) PastInitializedEvents( return events, nil } -func (b *Bridge) MovedFundsSweepTimedOutEvent( +func (b *Bridge) MovingFundsParametersUpdatedEvent( opts *ethereum.SubscribeOpts, - walletPubKeyHashFilter [][20]byte, -) *BMovedFundsSweepTimedOutSubscription { +) *BMovingFundsParametersUpdatedSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -8265,30 +11141,36 @@ func (b *Bridge) MovedFundsSweepTimedOutEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BMovedFundsSweepTimedOutSubscription{ + return &BMovingFundsParametersUpdatedSubscription{ b, opts, - walletPubKeyHashFilter, } } -type BMovedFundsSweepTimedOutSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts - walletPubKeyHashFilter [][20]byte +type BMovingFundsParametersUpdatedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts } -type bridgeMovedFundsSweepTimedOutFunc func( - WalletPubKeyHash [20]byte, - MovingFundsTxHash [32]byte, - MovingFundsTxOutputIndex uint32, +type bridgeMovingFundsParametersUpdatedFunc func( + MovingFundsTxMaxTotalFee uint64, + MovingFundsDustThreshold uint64, + MovingFundsTimeoutResetDelay uint32, + MovingFundsTimeout uint32, + MovingFundsTimeoutSlashingAmount *big.Int, + MovingFundsTimeoutNotifierRewardMultiplier uint32, + MovingFundsCommitmentGasOffset uint16, + MovedFundsSweepTxMaxTotalFee uint64, + MovedFundsSweepTimeout uint32, + MovedFundsSweepTimeoutSlashingAmount *big.Int, + MovedFundsSweepTimeoutNotifierRewardMultiplier uint32, blockNumber uint64, ) -func (mfstos *BMovedFundsSweepTimedOutSubscription) OnEvent( - handler bridgeMovedFundsSweepTimedOutFunc, +func (mfpus *BMovingFundsParametersUpdatedSubscription) OnEvent( + handler bridgeMovingFundsParametersUpdatedFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeMovedFundsSweepTimedOut) + eventChan := make(chan *abi.BridgeMovingFundsParametersUpdated) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -8298,52 +11180,59 @@ func (mfstos *BMovedFundsSweepTimedOutSubscription) OnEvent( return case event := <-eventChan: handler( - event.WalletPubKeyHash, - event.MovingFundsTxHash, - event.MovingFundsTxOutputIndex, + event.MovingFundsTxMaxTotalFee, + event.MovingFundsDustThreshold, + event.MovingFundsTimeoutResetDelay, + event.MovingFundsTimeout, + event.MovingFundsTimeoutSlashingAmount, + event.MovingFundsTimeoutNotifierRewardMultiplier, + event.MovingFundsCommitmentGasOffset, + event.MovedFundsSweepTxMaxTotalFee, + event.MovedFundsSweepTimeout, + event.MovedFundsSweepTimeoutSlashingAmount, + event.MovedFundsSweepTimeoutNotifierRewardMultiplier, event.Raw.BlockNumber, ) } } }() - sub := mfstos.Pipe(eventChan) + sub := mfpus.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (mfstos *BMovedFundsSweepTimedOutSubscription) Pipe( - sink chan *abi.BridgeMovedFundsSweepTimedOut, +func (mfpus *BMovingFundsParametersUpdatedSubscription) Pipe( + sink chan *abi.BridgeMovingFundsParametersUpdated, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(mfstos.opts.Tick) + ticker := time.NewTicker(mfpus.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := mfstos.contract.blockCounter.CurrentBlock() + lastBlock, err := mfpus.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - mfstos.opts.PastBlocks + fromBlock := lastBlock - mfpus.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past MovedFundsSweepTimedOut events "+ + "subscription monitoring fetching past MovingFundsParametersUpdated events "+ "starting from block [%v]", fromBlock, ) - events, err := mfstos.contract.PastMovedFundsSweepTimedOutEvents( + events, err := mfpus.contract.PastMovingFundsParametersUpdatedEvents( fromBlock, nil, - mfstos.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -8353,7 +11242,7 @@ func (mfstos *BMovedFundsSweepTimedOutSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past MovedFundsSweepTimedOut events", + "subscription monitoring fetched [%v] past MovingFundsParametersUpdated events", len(events), ) @@ -8364,9 +11253,8 @@ func (mfstos *BMovedFundsSweepTimedOutSubscription) Pipe( } }() - sub := mfstos.contract.watchMovedFundsSweepTimedOut( + sub := mfpus.contract.watchMovingFundsParametersUpdated( sink, - mfstos.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -8375,21 +11263,19 @@ func (mfstos *BMovedFundsSweepTimedOutSubscription) Pipe( }) } -func (b *Bridge) watchMovedFundsSweepTimedOut( - sink chan *abi.BridgeMovedFundsSweepTimedOut, - walletPubKeyHashFilter [][20]byte, +func (b *Bridge) watchMovingFundsParametersUpdated( + sink chan *abi.BridgeMovingFundsParametersUpdated, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchMovedFundsSweepTimedOut( + return b.contract.WatchMovingFundsParametersUpdated( &bind.WatchOpts{Context: ctx}, sink, - walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event MovedFundsSweepTimedOut had to be "+ + "subscription to event MovingFundsParametersUpdated had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -8398,7 +11284,7 @@ func (b *Bridge) watchMovedFundsSweepTimedOut( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event MovedFundsSweepTimedOut failed "+ + "subscription to event MovingFundsParametersUpdated failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -8414,26 +11300,24 @@ func (b *Bridge) watchMovedFundsSweepTimedOut( ) } -func (b *Bridge) PastMovedFundsSweepTimedOutEvents( +func (b *Bridge) PastMovingFundsParametersUpdatedEvents( startBlock uint64, endBlock *uint64, - walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeMovedFundsSweepTimedOut, error) { - iterator, err := b.contract.FilterMovedFundsSweepTimedOut( +) ([]*abi.BridgeMovingFundsParametersUpdated, error) { + iterator, err := b.contract.FilterMovingFundsParametersUpdated( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, - walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past MovedFundsSweepTimedOut events: [%v]", + "error retrieving past MovingFundsParametersUpdated events: [%v]", err, ) } - events := make([]*abi.BridgeMovedFundsSweepTimedOut, 0) + events := make([]*abi.BridgeMovingFundsParametersUpdated, 0) for iterator.Next() { event := iterator.Event @@ -8443,10 +11327,10 @@ func (b *Bridge) PastMovedFundsSweepTimedOutEvents( return events, nil } -func (b *Bridge) MovedFundsSweptEvent( +func (b *Bridge) MovingFundsTimedOutEvent( opts *ethereum.SubscribeOpts, walletPubKeyHashFilter [][20]byte, -) *BMovedFundsSweptSubscription { +) *BMovingFundsTimedOutSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -8457,29 +11341,28 @@ func (b *Bridge) MovedFundsSweptEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BMovedFundsSweptSubscription{ + return &BMovingFundsTimedOutSubscription{ b, opts, walletPubKeyHashFilter, } } -type BMovedFundsSweptSubscription struct { +type BMovingFundsTimedOutSubscription struct { contract *Bridge opts *ethereum.SubscribeOpts walletPubKeyHashFilter [][20]byte } -type bridgeMovedFundsSweptFunc func( +type bridgeMovingFundsTimedOutFunc func( WalletPubKeyHash [20]byte, - SweepTxHash [32]byte, blockNumber uint64, ) -func (mfss *BMovedFundsSweptSubscription) OnEvent( - handler bridgeMovedFundsSweptFunc, +func (mftos *BMovingFundsTimedOutSubscription) OnEvent( + handler bridgeMovingFundsTimedOutFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeMovedFundsSwept) + eventChan := make(chan *abi.BridgeMovingFundsTimedOut) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -8490,50 +11373,49 @@ func (mfss *BMovedFundsSweptSubscription) OnEvent( case event := <-eventChan: handler( event.WalletPubKeyHash, - event.SweepTxHash, event.Raw.BlockNumber, ) } } }() - sub := mfss.Pipe(eventChan) + sub := mftos.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (mfss *BMovedFundsSweptSubscription) Pipe( - sink chan *abi.BridgeMovedFundsSwept, +func (mftos *BMovingFundsTimedOutSubscription) Pipe( + sink chan *abi.BridgeMovingFundsTimedOut, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(mfss.opts.Tick) + ticker := time.NewTicker(mftos.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := mfss.contract.blockCounter.CurrentBlock() + lastBlock, err := mftos.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - mfss.opts.PastBlocks + fromBlock := lastBlock - mftos.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past MovedFundsSwept events "+ + "subscription monitoring fetching past MovingFundsTimedOut events "+ "starting from block [%v]", fromBlock, ) - events, err := mfss.contract.PastMovedFundsSweptEvents( + events, err := mftos.contract.PastMovingFundsTimedOutEvents( fromBlock, nil, - mfss.walletPubKeyHashFilter, + mftos.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -8543,7 +11425,7 @@ func (mfss *BMovedFundsSweptSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past MovedFundsSwept events", + "subscription monitoring fetched [%v] past MovingFundsTimedOut events", len(events), ) @@ -8554,9 +11436,9 @@ func (mfss *BMovedFundsSweptSubscription) Pipe( } }() - sub := mfss.contract.watchMovedFundsSwept( + sub := mftos.contract.watchMovingFundsTimedOut( sink, - mfss.walletPubKeyHashFilter, + mftos.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -8565,12 +11447,12 @@ func (mfss *BMovedFundsSweptSubscription) Pipe( }) } -func (b *Bridge) watchMovedFundsSwept( - sink chan *abi.BridgeMovedFundsSwept, +func (b *Bridge) watchMovingFundsTimedOut( + sink chan *abi.BridgeMovingFundsTimedOut, walletPubKeyHashFilter [][20]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchMovedFundsSwept( + return b.contract.WatchMovingFundsTimedOut( &bind.WatchOpts{Context: ctx}, sink, walletPubKeyHashFilter, @@ -8579,7 +11461,7 @@ func (b *Bridge) watchMovedFundsSwept( thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event MovedFundsSwept had to be "+ + "subscription to event MovingFundsTimedOut had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -8588,7 +11470,7 @@ func (b *Bridge) watchMovedFundsSwept( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event MovedFundsSwept failed "+ + "subscription to event MovingFundsTimedOut failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -8604,12 +11486,12 @@ func (b *Bridge) watchMovedFundsSwept( ) } -func (b *Bridge) PastMovedFundsSweptEvents( +func (b *Bridge) PastMovingFundsTimedOutEvents( startBlock uint64, endBlock *uint64, walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeMovedFundsSwept, error) { - iterator, err := b.contract.FilterMovedFundsSwept( +) ([]*abi.BridgeMovingFundsTimedOut, error) { + iterator, err := b.contract.FilterMovingFundsTimedOut( &bind.FilterOpts{ Start: startBlock, End: endBlock, @@ -8618,12 +11500,12 @@ func (b *Bridge) PastMovedFundsSweptEvents( ) if err != nil { return nil, fmt.Errorf( - "error retrieving past MovedFundsSwept events: [%v]", + "error retrieving past MovingFundsTimedOut events: [%v]", err, ) } - events := make([]*abi.BridgeMovedFundsSwept, 0) + events := make([]*abi.BridgeMovingFundsTimedOut, 0) for iterator.Next() { event := iterator.Event @@ -8633,10 +11515,10 @@ func (b *Bridge) PastMovedFundsSweptEvents( return events, nil } -func (b *Bridge) MovingFundsBelowDustReportedEvent( +func (b *Bridge) MovingFundsTimeoutResetEvent( opts *ethereum.SubscribeOpts, walletPubKeyHashFilter [][20]byte, -) *BMovingFundsBelowDustReportedSubscription { +) *BMovingFundsTimeoutResetSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -8647,28 +11529,28 @@ func (b *Bridge) MovingFundsBelowDustReportedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BMovingFundsBelowDustReportedSubscription{ + return &BMovingFundsTimeoutResetSubscription{ b, opts, walletPubKeyHashFilter, } } -type BMovingFundsBelowDustReportedSubscription struct { +type BMovingFundsTimeoutResetSubscription struct { contract *Bridge opts *ethereum.SubscribeOpts walletPubKeyHashFilter [][20]byte } -type bridgeMovingFundsBelowDustReportedFunc func( +type bridgeMovingFundsTimeoutResetFunc func( WalletPubKeyHash [20]byte, blockNumber uint64, ) -func (mfbdrs *BMovingFundsBelowDustReportedSubscription) OnEvent( - handler bridgeMovingFundsBelowDustReportedFunc, +func (mftrs *BMovingFundsTimeoutResetSubscription) OnEvent( + handler bridgeMovingFundsTimeoutResetFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeMovingFundsBelowDustReported) + eventChan := make(chan *abi.BridgeMovingFundsTimeoutReset) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -8685,43 +11567,43 @@ func (mfbdrs *BMovingFundsBelowDustReportedSubscription) OnEvent( } }() - sub := mfbdrs.Pipe(eventChan) + sub := mftrs.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (mfbdrs *BMovingFundsBelowDustReportedSubscription) Pipe( - sink chan *abi.BridgeMovingFundsBelowDustReported, +func (mftrs *BMovingFundsTimeoutResetSubscription) Pipe( + sink chan *abi.BridgeMovingFundsTimeoutReset, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(mfbdrs.opts.Tick) + ticker := time.NewTicker(mftrs.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := mfbdrs.contract.blockCounter.CurrentBlock() + lastBlock, err := mftrs.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - mfbdrs.opts.PastBlocks + fromBlock := lastBlock - mftrs.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past MovingFundsBelowDustReported events "+ + "subscription monitoring fetching past MovingFundsTimeoutReset events "+ "starting from block [%v]", fromBlock, ) - events, err := mfbdrs.contract.PastMovingFundsBelowDustReportedEvents( + events, err := mftrs.contract.PastMovingFundsTimeoutResetEvents( fromBlock, nil, - mfbdrs.walletPubKeyHashFilter, + mftrs.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -8731,7 +11613,7 @@ func (mfbdrs *BMovingFundsBelowDustReportedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past MovingFundsBelowDustReported events", + "subscription monitoring fetched [%v] past MovingFundsTimeoutReset events", len(events), ) @@ -8742,9 +11624,9 @@ func (mfbdrs *BMovingFundsBelowDustReportedSubscription) Pipe( } }() - sub := mfbdrs.contract.watchMovingFundsBelowDustReported( + sub := mftrs.contract.watchMovingFundsTimeoutReset( sink, - mfbdrs.walletPubKeyHashFilter, + mftrs.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -8753,12 +11635,12 @@ func (mfbdrs *BMovingFundsBelowDustReportedSubscription) Pipe( }) } -func (b *Bridge) watchMovingFundsBelowDustReported( - sink chan *abi.BridgeMovingFundsBelowDustReported, +func (b *Bridge) watchMovingFundsTimeoutReset( + sink chan *abi.BridgeMovingFundsTimeoutReset, walletPubKeyHashFilter [][20]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchMovingFundsBelowDustReported( + return b.contract.WatchMovingFundsTimeoutReset( &bind.WatchOpts{Context: ctx}, sink, walletPubKeyHashFilter, @@ -8767,7 +11649,7 @@ func (b *Bridge) watchMovingFundsBelowDustReported( thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event MovingFundsBelowDustReported had to be "+ + "subscription to event MovingFundsTimeoutReset had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -8776,7 +11658,7 @@ func (b *Bridge) watchMovingFundsBelowDustReported( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event MovingFundsBelowDustReported failed "+ + "subscription to event MovingFundsTimeoutReset failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -8792,12 +11674,12 @@ func (b *Bridge) watchMovingFundsBelowDustReported( ) } -func (b *Bridge) PastMovingFundsBelowDustReportedEvents( +func (b *Bridge) PastMovingFundsTimeoutResetEvents( startBlock uint64, endBlock *uint64, walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeMovingFundsBelowDustReported, error) { - iterator, err := b.contract.FilterMovingFundsBelowDustReported( +) ([]*abi.BridgeMovingFundsTimeoutReset, error) { + iterator, err := b.contract.FilterMovingFundsTimeoutReset( &bind.FilterOpts{ Start: startBlock, End: endBlock, @@ -8806,12 +11688,12 @@ func (b *Bridge) PastMovingFundsBelowDustReportedEvents( ) if err != nil { return nil, fmt.Errorf( - "error retrieving past MovingFundsBelowDustReported events: [%v]", + "error retrieving past MovingFundsTimeoutReset events: [%v]", err, ) } - events := make([]*abi.BridgeMovingFundsBelowDustReported, 0) + events := make([]*abi.BridgeMovingFundsTimeoutReset, 0) for iterator.Next() { event := iterator.Event @@ -8821,10 +11703,12 @@ func (b *Bridge) PastMovingFundsBelowDustReportedEvents( return events, nil } -func (b *Bridge) MovingFundsCommitmentSubmittedEvent( +func (b *Bridge) NewFrostWalletRegisteredEvent( opts *ethereum.SubscribeOpts, + walletIDFilter [][32]byte, walletPubKeyHashFilter [][20]byte, -) *BMovingFundsCommitmentSubmittedSubscription { + xOnlyOutputKeyFilter [][32]byte, +) *BNewFrostWalletRegisteredSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -8835,30 +11719,34 @@ func (b *Bridge) MovingFundsCommitmentSubmittedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BMovingFundsCommitmentSubmittedSubscription{ + return &BNewFrostWalletRegisteredSubscription{ b, opts, + walletIDFilter, walletPubKeyHashFilter, + xOnlyOutputKeyFilter, } } -type BMovingFundsCommitmentSubmittedSubscription struct { +type BNewFrostWalletRegisteredSubscription struct { contract *Bridge opts *ethereum.SubscribeOpts + walletIDFilter [][32]byte walletPubKeyHashFilter [][20]byte + xOnlyOutputKeyFilter [][32]byte } -type bridgeMovingFundsCommitmentSubmittedFunc func( +type bridgeNewFrostWalletRegisteredFunc func( + WalletID [32]byte, WalletPubKeyHash [20]byte, - TargetWallets [][20]byte, - Submitter common.Address, + XOnlyOutputKey [32]byte, blockNumber uint64, ) -func (mfcss *BMovingFundsCommitmentSubmittedSubscription) OnEvent( - handler bridgeMovingFundsCommitmentSubmittedFunc, +func (nfwrs *BNewFrostWalletRegisteredSubscription) OnEvent( + handler bridgeNewFrostWalletRegisteredFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeMovingFundsCommitmentSubmitted) + eventChan := make(chan *abi.BridgeNewFrostWalletRegistered) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -8868,52 +11756,54 @@ func (mfcss *BMovingFundsCommitmentSubmittedSubscription) OnEvent( return case event := <-eventChan: handler( + event.WalletID, event.WalletPubKeyHash, - event.TargetWallets, - event.Submitter, + event.XOnlyOutputKey, event.Raw.BlockNumber, ) } } }() - sub := mfcss.Pipe(eventChan) + sub := nfwrs.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (mfcss *BMovingFundsCommitmentSubmittedSubscription) Pipe( - sink chan *abi.BridgeMovingFundsCommitmentSubmitted, +func (nfwrs *BNewFrostWalletRegisteredSubscription) Pipe( + sink chan *abi.BridgeNewFrostWalletRegistered, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(mfcss.opts.Tick) + ticker := time.NewTicker(nfwrs.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := mfcss.contract.blockCounter.CurrentBlock() + lastBlock, err := nfwrs.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - mfcss.opts.PastBlocks + fromBlock := lastBlock - nfwrs.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past MovingFundsCommitmentSubmitted events "+ + "subscription monitoring fetching past NewFrostWalletRegistered events "+ "starting from block [%v]", fromBlock, ) - events, err := mfcss.contract.PastMovingFundsCommitmentSubmittedEvents( + events, err := nfwrs.contract.PastNewFrostWalletRegisteredEvents( fromBlock, nil, - mfcss.walletPubKeyHashFilter, + nfwrs.walletIDFilter, + nfwrs.walletPubKeyHashFilter, + nfwrs.xOnlyOutputKeyFilter, ) if err != nil { bLogger.Errorf( @@ -8923,7 +11813,7 @@ func (mfcss *BMovingFundsCommitmentSubmittedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past MovingFundsCommitmentSubmitted events", + "subscription monitoring fetched [%v] past NewFrostWalletRegistered events", len(events), ) @@ -8934,9 +11824,11 @@ func (mfcss *BMovingFundsCommitmentSubmittedSubscription) Pipe( } }() - sub := mfcss.contract.watchMovingFundsCommitmentSubmitted( + sub := nfwrs.contract.watchNewFrostWalletRegistered( sink, - mfcss.walletPubKeyHashFilter, + nfwrs.walletIDFilter, + nfwrs.walletPubKeyHashFilter, + nfwrs.xOnlyOutputKeyFilter, ) return subscription.NewEventSubscription(func() { @@ -8945,21 +11837,25 @@ func (mfcss *BMovingFundsCommitmentSubmittedSubscription) Pipe( }) } -func (b *Bridge) watchMovingFundsCommitmentSubmitted( - sink chan *abi.BridgeMovingFundsCommitmentSubmitted, +func (b *Bridge) watchNewFrostWalletRegistered( + sink chan *abi.BridgeNewFrostWalletRegistered, + walletIDFilter [][32]byte, walletPubKeyHashFilter [][20]byte, + xOnlyOutputKeyFilter [][32]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchMovingFundsCommitmentSubmitted( + return b.contract.WatchNewFrostWalletRegistered( &bind.WatchOpts{Context: ctx}, sink, + walletIDFilter, walletPubKeyHashFilter, + xOnlyOutputKeyFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event MovingFundsCommitmentSubmitted had to be "+ + "subscription to event NewFrostWalletRegistered had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -8968,7 +11864,7 @@ func (b *Bridge) watchMovingFundsCommitmentSubmitted( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event MovingFundsCommitmentSubmitted failed "+ + "subscription to event NewFrostWalletRegistered failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -8984,26 +11880,30 @@ func (b *Bridge) watchMovingFundsCommitmentSubmitted( ) } -func (b *Bridge) PastMovingFundsCommitmentSubmittedEvents( +func (b *Bridge) PastNewFrostWalletRegisteredEvents( startBlock uint64, endBlock *uint64, + walletIDFilter [][32]byte, walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeMovingFundsCommitmentSubmitted, error) { - iterator, err := b.contract.FilterMovingFundsCommitmentSubmitted( + xOnlyOutputKeyFilter [][32]byte, +) ([]*abi.BridgeNewFrostWalletRegistered, error) { + iterator, err := b.contract.FilterNewFrostWalletRegistered( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, + walletIDFilter, walletPubKeyHashFilter, + xOnlyOutputKeyFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past MovingFundsCommitmentSubmitted events: [%v]", + "error retrieving past NewFrostWalletRegistered events: [%v]", err, ) } - events := make([]*abi.BridgeMovingFundsCommitmentSubmitted, 0) + events := make([]*abi.BridgeNewFrostWalletRegistered, 0) for iterator.Next() { event := iterator.Event @@ -9013,10 +11913,11 @@ func (b *Bridge) PastMovingFundsCommitmentSubmittedEvents( return events, nil } -func (b *Bridge) MovingFundsCompletedEvent( +func (b *Bridge) NewWalletRegisteredEvent( opts *ethereum.SubscribeOpts, + ecdsaWalletIDFilter [][32]byte, walletPubKeyHashFilter [][20]byte, -) *BMovingFundsCompletedSubscription { +) *BNewWalletRegisteredSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -9027,29 +11928,31 @@ func (b *Bridge) MovingFundsCompletedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BMovingFundsCompletedSubscription{ + return &BNewWalletRegisteredSubscription{ b, opts, + ecdsaWalletIDFilter, walletPubKeyHashFilter, } } -type BMovingFundsCompletedSubscription struct { +type BNewWalletRegisteredSubscription struct { contract *Bridge opts *ethereum.SubscribeOpts + ecdsaWalletIDFilter [][32]byte walletPubKeyHashFilter [][20]byte } -type bridgeMovingFundsCompletedFunc func( +type bridgeNewWalletRegisteredFunc func( + EcdsaWalletID [32]byte, WalletPubKeyHash [20]byte, - MovingFundsTxHash [32]byte, blockNumber uint64, ) -func (mfcs *BMovingFundsCompletedSubscription) OnEvent( - handler bridgeMovingFundsCompletedFunc, +func (nwrs *BNewWalletRegisteredSubscription) OnEvent( + handler bridgeNewWalletRegisteredFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeMovingFundsCompleted) + eventChan := make(chan *abi.BridgeNewWalletRegistered) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -9059,51 +11962,52 @@ func (mfcs *BMovingFundsCompletedSubscription) OnEvent( return case event := <-eventChan: handler( + event.EcdsaWalletID, event.WalletPubKeyHash, - event.MovingFundsTxHash, event.Raw.BlockNumber, ) } } }() - sub := mfcs.Pipe(eventChan) + sub := nwrs.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (mfcs *BMovingFundsCompletedSubscription) Pipe( - sink chan *abi.BridgeMovingFundsCompleted, +func (nwrs *BNewWalletRegisteredSubscription) Pipe( + sink chan *abi.BridgeNewWalletRegistered, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(mfcs.opts.Tick) + ticker := time.NewTicker(nwrs.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := mfcs.contract.blockCounter.CurrentBlock() + lastBlock, err := nwrs.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - mfcs.opts.PastBlocks + fromBlock := lastBlock - nwrs.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past MovingFundsCompleted events "+ + "subscription monitoring fetching past NewWalletRegistered events "+ "starting from block [%v]", fromBlock, ) - events, err := mfcs.contract.PastMovingFundsCompletedEvents( + events, err := nwrs.contract.PastNewWalletRegisteredEvents( fromBlock, nil, - mfcs.walletPubKeyHashFilter, + nwrs.ecdsaWalletIDFilter, + nwrs.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -9113,7 +12017,7 @@ func (mfcs *BMovingFundsCompletedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past MovingFundsCompleted events", + "subscription monitoring fetched [%v] past NewWalletRegistered events", len(events), ) @@ -9124,9 +12028,10 @@ func (mfcs *BMovingFundsCompletedSubscription) Pipe( } }() - sub := mfcs.contract.watchMovingFundsCompleted( + sub := nwrs.contract.watchNewWalletRegistered( sink, - mfcs.walletPubKeyHashFilter, + nwrs.ecdsaWalletIDFilter, + nwrs.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -9135,21 +12040,23 @@ func (mfcs *BMovingFundsCompletedSubscription) Pipe( }) } -func (b *Bridge) watchMovingFundsCompleted( - sink chan *abi.BridgeMovingFundsCompleted, +func (b *Bridge) watchNewWalletRegistered( + sink chan *abi.BridgeNewWalletRegistered, + ecdsaWalletIDFilter [][32]byte, walletPubKeyHashFilter [][20]byte, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchMovingFundsCompleted( + return b.contract.WatchNewWalletRegistered( &bind.WatchOpts{Context: ctx}, sink, + ecdsaWalletIDFilter, walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event MovingFundsCompleted had to be "+ + "subscription to event NewWalletRegistered had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -9158,7 +12065,7 @@ func (b *Bridge) watchMovingFundsCompleted( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event MovingFundsCompleted failed "+ + "subscription to event NewWalletRegistered failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -9174,26 +12081,28 @@ func (b *Bridge) watchMovingFundsCompleted( ) } -func (b *Bridge) PastMovingFundsCompletedEvents( +func (b *Bridge) PastNewWalletRegisteredEvents( startBlock uint64, endBlock *uint64, + ecdsaWalletIDFilter [][32]byte, walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeMovingFundsCompleted, error) { - iterator, err := b.contract.FilterMovingFundsCompleted( +) ([]*abi.BridgeNewWalletRegistered, error) { + iterator, err := b.contract.FilterNewWalletRegistered( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, + ecdsaWalletIDFilter, walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past MovingFundsCompleted events: [%v]", + "error retrieving past NewWalletRegistered events: [%v]", err, ) } - events := make([]*abi.BridgeMovingFundsCompleted, 0) + events := make([]*abi.BridgeNewWalletRegistered, 0) for iterator.Next() { event := iterator.Event @@ -9203,9 +12112,12 @@ func (b *Bridge) PastMovingFundsCompletedEvents( return events, nil } -func (b *Bridge) MovingFundsParametersUpdatedEvent( +func (b *Bridge) NewWalletRegisteredV2Event( opts *ethereum.SubscribeOpts, -) *BMovingFundsParametersUpdatedSubscription { + walletIDFilter [][32]byte, + ecdsaWalletIDFilter [][32]byte, + walletPubKeyHashFilter [][20]byte, +) *BNewWalletRegisteredV2Subscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -9216,36 +12128,34 @@ func (b *Bridge) MovingFundsParametersUpdatedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BMovingFundsParametersUpdatedSubscription{ + return &BNewWalletRegisteredV2Subscription{ b, opts, + walletIDFilter, + ecdsaWalletIDFilter, + walletPubKeyHashFilter, } } -type BMovingFundsParametersUpdatedSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts +type BNewWalletRegisteredV2Subscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + walletIDFilter [][32]byte + ecdsaWalletIDFilter [][32]byte + walletPubKeyHashFilter [][20]byte } -type bridgeMovingFundsParametersUpdatedFunc func( - MovingFundsTxMaxTotalFee uint64, - MovingFundsDustThreshold uint64, - MovingFundsTimeoutResetDelay uint32, - MovingFundsTimeout uint32, - MovingFundsTimeoutSlashingAmount *big.Int, - MovingFundsTimeoutNotifierRewardMultiplier uint32, - MovingFundsCommitmentGasOffset uint16, - MovedFundsSweepTxMaxTotalFee uint64, - MovedFundsSweepTimeout uint32, - MovedFundsSweepTimeoutSlashingAmount *big.Int, - MovedFundsSweepTimeoutNotifierRewardMultiplier uint32, +type bridgeNewWalletRegisteredV2Func func( + WalletID [32]byte, + EcdsaWalletID [32]byte, + WalletPubKeyHash [20]byte, blockNumber uint64, ) -func (mfpus *BMovingFundsParametersUpdatedSubscription) OnEvent( - handler bridgeMovingFundsParametersUpdatedFunc, +func (nwrvs *BNewWalletRegisteredV2Subscription) OnEvent( + handler bridgeNewWalletRegisteredV2Func, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeMovingFundsParametersUpdated) + eventChan := make(chan *abi.BridgeNewWalletRegisteredV2) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -9255,59 +12165,54 @@ func (mfpus *BMovingFundsParametersUpdatedSubscription) OnEvent( return case event := <-eventChan: handler( - event.MovingFundsTxMaxTotalFee, - event.MovingFundsDustThreshold, - event.MovingFundsTimeoutResetDelay, - event.MovingFundsTimeout, - event.MovingFundsTimeoutSlashingAmount, - event.MovingFundsTimeoutNotifierRewardMultiplier, - event.MovingFundsCommitmentGasOffset, - event.MovedFundsSweepTxMaxTotalFee, - event.MovedFundsSweepTimeout, - event.MovedFundsSweepTimeoutSlashingAmount, - event.MovedFundsSweepTimeoutNotifierRewardMultiplier, + event.WalletID, + event.EcdsaWalletID, + event.WalletPubKeyHash, event.Raw.BlockNumber, ) } } }() - sub := mfpus.Pipe(eventChan) + sub := nwrvs.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (mfpus *BMovingFundsParametersUpdatedSubscription) Pipe( - sink chan *abi.BridgeMovingFundsParametersUpdated, +func (nwrvs *BNewWalletRegisteredV2Subscription) Pipe( + sink chan *abi.BridgeNewWalletRegisteredV2, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(mfpus.opts.Tick) + ticker := time.NewTicker(nwrvs.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := mfpus.contract.blockCounter.CurrentBlock() + lastBlock, err := nwrvs.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - mfpus.opts.PastBlocks + fromBlock := lastBlock - nwrvs.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past MovingFundsParametersUpdated events "+ + "subscription monitoring fetching past NewWalletRegisteredV2 events "+ "starting from block [%v]", fromBlock, ) - events, err := mfpus.contract.PastMovingFundsParametersUpdatedEvents( + events, err := nwrvs.contract.PastNewWalletRegisteredV2Events( fromBlock, nil, + nwrvs.walletIDFilter, + nwrvs.ecdsaWalletIDFilter, + nwrvs.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -9317,7 +12222,7 @@ func (mfpus *BMovingFundsParametersUpdatedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past MovingFundsParametersUpdated events", + "subscription monitoring fetched [%v] past NewWalletRegisteredV2 events", len(events), ) @@ -9328,8 +12233,11 @@ func (mfpus *BMovingFundsParametersUpdatedSubscription) Pipe( } }() - sub := mfpus.contract.watchMovingFundsParametersUpdated( + sub := nwrvs.contract.watchNewWalletRegisteredV2( sink, + nwrvs.walletIDFilter, + nwrvs.ecdsaWalletIDFilter, + nwrvs.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -9338,19 +12246,25 @@ func (mfpus *BMovingFundsParametersUpdatedSubscription) Pipe( }) } -func (b *Bridge) watchMovingFundsParametersUpdated( - sink chan *abi.BridgeMovingFundsParametersUpdated, +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.WatchMovingFundsParametersUpdated( + return b.contract.WatchNewWalletRegisteredV2( &bind.WatchOpts{Context: ctx}, sink, + walletIDFilter, + ecdsaWalletIDFilter, + walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event MovingFundsParametersUpdated had to be "+ + "subscription to event NewWalletRegisteredV2 had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -9359,7 +12273,7 @@ func (b *Bridge) watchMovingFundsParametersUpdated( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event MovingFundsParametersUpdated failed "+ + "subscription to event NewWalletRegisteredV2 failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -9375,24 +12289,30 @@ func (b *Bridge) watchMovingFundsParametersUpdated( ) } -func (b *Bridge) PastMovingFundsParametersUpdatedEvents( +func (b *Bridge) PastNewWalletRegisteredV2Events( startBlock uint64, endBlock *uint64, -) ([]*abi.BridgeMovingFundsParametersUpdated, error) { - iterator, err := b.contract.FilterMovingFundsParametersUpdated( + 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 MovingFundsParametersUpdated events: [%v]", + "error retrieving past NewWalletRegisteredV2 events: [%v]", err, ) } - events := make([]*abi.BridgeMovingFundsParametersUpdated, 0) + events := make([]*abi.BridgeNewWalletRegisteredV2, 0) for iterator.Next() { event := iterator.Event @@ -9402,10 +12322,9 @@ func (b *Bridge) PastMovingFundsParametersUpdatedEvents( return events, nil } -func (b *Bridge) MovingFundsTimedOutEvent( +func (b *Bridge) NewWalletRequestedEvent( opts *ethereum.SubscribeOpts, - walletPubKeyHashFilter [][20]byte, -) *BMovingFundsTimedOutSubscription { +) *BNewWalletRequestedSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -9416,28 +12335,25 @@ func (b *Bridge) MovingFundsTimedOutEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BMovingFundsTimedOutSubscription{ + return &BNewWalletRequestedSubscription{ b, opts, - walletPubKeyHashFilter, } } -type BMovingFundsTimedOutSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts - walletPubKeyHashFilter [][20]byte +type BNewWalletRequestedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts } -type bridgeMovingFundsTimedOutFunc func( - WalletPubKeyHash [20]byte, +type bridgeNewWalletRequestedFunc func( blockNumber uint64, ) -func (mftos *BMovingFundsTimedOutSubscription) OnEvent( - handler bridgeMovingFundsTimedOutFunc, +func (nwrs *BNewWalletRequestedSubscription) OnEvent( + handler bridgeNewWalletRequestedFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeMovingFundsTimedOut) + eventChan := make(chan *abi.BridgeNewWalletRequested) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -9447,50 +12363,48 @@ func (mftos *BMovingFundsTimedOutSubscription) OnEvent( return case event := <-eventChan: handler( - event.WalletPubKeyHash, event.Raw.BlockNumber, ) } } }() - sub := mftos.Pipe(eventChan) + sub := nwrs.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (mftos *BMovingFundsTimedOutSubscription) Pipe( - sink chan *abi.BridgeMovingFundsTimedOut, +func (nwrs *BNewWalletRequestedSubscription) Pipe( + sink chan *abi.BridgeNewWalletRequested, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(mftos.opts.Tick) + ticker := time.NewTicker(nwrs.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := mftos.contract.blockCounter.CurrentBlock() + lastBlock, err := nwrs.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - mftos.opts.PastBlocks + fromBlock := lastBlock - nwrs.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past MovingFundsTimedOut events "+ + "subscription monitoring fetching past NewWalletRequested events "+ "starting from block [%v]", fromBlock, ) - events, err := mftos.contract.PastMovingFundsTimedOutEvents( + events, err := nwrs.contract.PastNewWalletRequestedEvents( fromBlock, nil, - mftos.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -9500,7 +12414,7 @@ func (mftos *BMovingFundsTimedOutSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past MovingFundsTimedOut events", + "subscription monitoring fetched [%v] past NewWalletRequested events", len(events), ) @@ -9511,9 +12425,8 @@ func (mftos *BMovingFundsTimedOutSubscription) Pipe( } }() - sub := mftos.contract.watchMovingFundsTimedOut( + sub := nwrs.contract.watchNewWalletRequested( sink, - mftos.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -9522,21 +12435,19 @@ func (mftos *BMovingFundsTimedOutSubscription) Pipe( }) } -func (b *Bridge) watchMovingFundsTimedOut( - sink chan *abi.BridgeMovingFundsTimedOut, - walletPubKeyHashFilter [][20]byte, +func (b *Bridge) watchNewWalletRequested( + sink chan *abi.BridgeNewWalletRequested, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchMovingFundsTimedOut( + return b.contract.WatchNewWalletRequested( &bind.WatchOpts{Context: ctx}, sink, - walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event MovingFundsTimedOut had to be "+ + "subscription to event NewWalletRequested had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -9545,7 +12456,7 @@ func (b *Bridge) watchMovingFundsTimedOut( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event MovingFundsTimedOut failed "+ + "subscription to event NewWalletRequested failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -9561,26 +12472,24 @@ func (b *Bridge) watchMovingFundsTimedOut( ) } -func (b *Bridge) PastMovingFundsTimedOutEvents( +func (b *Bridge) PastNewWalletRequestedEvents( startBlock uint64, endBlock *uint64, - walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeMovingFundsTimedOut, error) { - iterator, err := b.contract.FilterMovingFundsTimedOut( +) ([]*abi.BridgeNewWalletRequested, error) { + iterator, err := b.contract.FilterNewWalletRequested( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, - walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past MovingFundsTimedOut events: [%v]", + "error retrieving past NewWalletRequested events: [%v]", err, ) } - events := make([]*abi.BridgeMovingFundsTimedOut, 0) + events := make([]*abi.BridgeNewWalletRequested, 0) for iterator.Next() { event := iterator.Event @@ -9590,10 +12499,10 @@ func (b *Bridge) PastMovingFundsTimedOutEvents( return events, nil } -func (b *Bridge) MovingFundsTimeoutResetEvent( +func (b *Bridge) NewWalletSchemeSetEvent( opts *ethereum.SubscribeOpts, - walletPubKeyHashFilter [][20]byte, -) *BMovingFundsTimeoutResetSubscription { + schemeFilter []uint8, +) *BNewWalletSchemeSetSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -9604,28 +12513,28 @@ func (b *Bridge) MovingFundsTimeoutResetEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BMovingFundsTimeoutResetSubscription{ + return &BNewWalletSchemeSetSubscription{ b, opts, - walletPubKeyHashFilter, + schemeFilter, } } -type BMovingFundsTimeoutResetSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts - walletPubKeyHashFilter [][20]byte +type BNewWalletSchemeSetSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + schemeFilter []uint8 } -type bridgeMovingFundsTimeoutResetFunc func( - WalletPubKeyHash [20]byte, +type bridgeNewWalletSchemeSetFunc func( + Scheme uint8, blockNumber uint64, ) -func (mftrs *BMovingFundsTimeoutResetSubscription) OnEvent( - handler bridgeMovingFundsTimeoutResetFunc, +func (nwsss *BNewWalletSchemeSetSubscription) OnEvent( + handler bridgeNewWalletSchemeSetFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeMovingFundsTimeoutReset) + eventChan := make(chan *abi.BridgeNewWalletSchemeSet) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -9635,50 +12544,50 @@ func (mftrs *BMovingFundsTimeoutResetSubscription) OnEvent( return case event := <-eventChan: handler( - event.WalletPubKeyHash, + event.Scheme, event.Raw.BlockNumber, ) } } }() - sub := mftrs.Pipe(eventChan) + sub := nwsss.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (mftrs *BMovingFundsTimeoutResetSubscription) Pipe( - sink chan *abi.BridgeMovingFundsTimeoutReset, +func (nwsss *BNewWalletSchemeSetSubscription) Pipe( + sink chan *abi.BridgeNewWalletSchemeSet, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(mftrs.opts.Tick) + ticker := time.NewTicker(nwsss.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := mftrs.contract.blockCounter.CurrentBlock() + lastBlock, err := nwsss.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - mftrs.opts.PastBlocks + fromBlock := lastBlock - nwsss.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past MovingFundsTimeoutReset events "+ + "subscription monitoring fetching past NewWalletSchemeSet events "+ "starting from block [%v]", fromBlock, ) - events, err := mftrs.contract.PastMovingFundsTimeoutResetEvents( + events, err := nwsss.contract.PastNewWalletSchemeSetEvents( fromBlock, nil, - mftrs.walletPubKeyHashFilter, + nwsss.schemeFilter, ) if err != nil { bLogger.Errorf( @@ -9688,7 +12597,7 @@ func (mftrs *BMovingFundsTimeoutResetSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past MovingFundsTimeoutReset events", + "subscription monitoring fetched [%v] past NewWalletSchemeSet events", len(events), ) @@ -9699,9 +12608,9 @@ func (mftrs *BMovingFundsTimeoutResetSubscription) Pipe( } }() - sub := mftrs.contract.watchMovingFundsTimeoutReset( + sub := nwsss.contract.watchNewWalletSchemeSet( sink, - mftrs.walletPubKeyHashFilter, + nwsss.schemeFilter, ) return subscription.NewEventSubscription(func() { @@ -9710,21 +12619,21 @@ func (mftrs *BMovingFundsTimeoutResetSubscription) Pipe( }) } -func (b *Bridge) watchMovingFundsTimeoutReset( - sink chan *abi.BridgeMovingFundsTimeoutReset, - walletPubKeyHashFilter [][20]byte, +func (b *Bridge) watchNewWalletSchemeSet( + sink chan *abi.BridgeNewWalletSchemeSet, + schemeFilter []uint8, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchMovingFundsTimeoutReset( + return b.contract.WatchNewWalletSchemeSet( &bind.WatchOpts{Context: ctx}, sink, - walletPubKeyHashFilter, + schemeFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event MovingFundsTimeoutReset had to be "+ + "subscription to event NewWalletSchemeSet had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -9733,7 +12642,7 @@ func (b *Bridge) watchMovingFundsTimeoutReset( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event MovingFundsTimeoutReset failed "+ + "subscription to event NewWalletSchemeSet failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -9749,26 +12658,26 @@ func (b *Bridge) watchMovingFundsTimeoutReset( ) } -func (b *Bridge) PastMovingFundsTimeoutResetEvents( +func (b *Bridge) PastNewWalletSchemeSetEvents( startBlock uint64, endBlock *uint64, - walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeMovingFundsTimeoutReset, error) { - iterator, err := b.contract.FilterMovingFundsTimeoutReset( + schemeFilter []uint8, +) ([]*abi.BridgeNewWalletSchemeSet, error) { + iterator, err := b.contract.FilterNewWalletSchemeSet( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, - walletPubKeyHashFilter, + schemeFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past MovingFundsTimeoutReset events: [%v]", + "error retrieving past NewWalletSchemeSet events: [%v]", err, ) } - events := make([]*abi.BridgeMovingFundsTimeoutReset, 0) + events := make([]*abi.BridgeNewWalletSchemeSet, 0) for iterator.Next() { event := iterator.Event @@ -9778,11 +12687,9 @@ func (b *Bridge) PastMovingFundsTimeoutResetEvents( return events, nil } -func (b *Bridge) NewWalletRegisteredEvent( +func (b *Bridge) P2TRFraudRouterSetEvent( opts *ethereum.SubscribeOpts, - ecdsaWalletIDFilter [][32]byte, - walletPubKeyHashFilter [][20]byte, -) *BNewWalletRegisteredSubscription { +) *BP2TRFraudRouterSetSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -9793,31 +12700,26 @@ func (b *Bridge) NewWalletRegisteredEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BNewWalletRegisteredSubscription{ + return &BP2TRFraudRouterSetSubscription{ b, opts, - ecdsaWalletIDFilter, - walletPubKeyHashFilter, } } -type BNewWalletRegisteredSubscription struct { - contract *Bridge - opts *ethereum.SubscribeOpts - ecdsaWalletIDFilter [][32]byte - walletPubKeyHashFilter [][20]byte +type BP2TRFraudRouterSetSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts } -type bridgeNewWalletRegisteredFunc func( - EcdsaWalletID [32]byte, - WalletPubKeyHash [20]byte, +type bridgeP2TRFraudRouterSetFunc func( + P2trFraudRouter common.Address, blockNumber uint64, ) -func (nwrs *BNewWalletRegisteredSubscription) OnEvent( - handler bridgeNewWalletRegisteredFunc, +func (ptrfrss *BP2TRFraudRouterSetSubscription) OnEvent( + handler bridgeP2TRFraudRouterSetFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeNewWalletRegistered) + eventChan := make(chan *abi.BridgeP2TRFraudRouterSet) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -9827,52 +12729,49 @@ func (nwrs *BNewWalletRegisteredSubscription) OnEvent( return case event := <-eventChan: handler( - event.EcdsaWalletID, - event.WalletPubKeyHash, + event.P2trFraudRouter, event.Raw.BlockNumber, ) } } }() - sub := nwrs.Pipe(eventChan) + sub := ptrfrss.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (nwrs *BNewWalletRegisteredSubscription) Pipe( - sink chan *abi.BridgeNewWalletRegistered, +func (ptrfrss *BP2TRFraudRouterSetSubscription) Pipe( + sink chan *abi.BridgeP2TRFraudRouterSet, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(nwrs.opts.Tick) + ticker := time.NewTicker(ptrfrss.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := nwrs.contract.blockCounter.CurrentBlock() + lastBlock, err := ptrfrss.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - nwrs.opts.PastBlocks + fromBlock := lastBlock - ptrfrss.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past NewWalletRegistered events "+ + "subscription monitoring fetching past P2TRFraudRouterSet events "+ "starting from block [%v]", fromBlock, ) - events, err := nwrs.contract.PastNewWalletRegisteredEvents( + events, err := ptrfrss.contract.PastP2TRFraudRouterSetEvents( fromBlock, nil, - nwrs.ecdsaWalletIDFilter, - nwrs.walletPubKeyHashFilter, ) if err != nil { bLogger.Errorf( @@ -9882,7 +12781,7 @@ func (nwrs *BNewWalletRegisteredSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past NewWalletRegistered events", + "subscription monitoring fetched [%v] past P2TRFraudRouterSet events", len(events), ) @@ -9893,10 +12792,8 @@ func (nwrs *BNewWalletRegisteredSubscription) Pipe( } }() - sub := nwrs.contract.watchNewWalletRegistered( + sub := ptrfrss.contract.watchP2TRFraudRouterSet( sink, - nwrs.ecdsaWalletIDFilter, - nwrs.walletPubKeyHashFilter, ) return subscription.NewEventSubscription(func() { @@ -9905,23 +12802,19 @@ func (nwrs *BNewWalletRegisteredSubscription) Pipe( }) } -func (b *Bridge) watchNewWalletRegistered( - sink chan *abi.BridgeNewWalletRegistered, - ecdsaWalletIDFilter [][32]byte, - walletPubKeyHashFilter [][20]byte, +func (b *Bridge) watchP2TRFraudRouterSet( + sink chan *abi.BridgeP2TRFraudRouterSet, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchNewWalletRegistered( + return b.contract.WatchP2TRFraudRouterSet( &bind.WatchOpts{Context: ctx}, sink, - ecdsaWalletIDFilter, - walletPubKeyHashFilter, ) } thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event NewWalletRegistered had to be "+ + "subscription to event P2TRFraudRouterSet had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -9930,7 +12823,7 @@ func (b *Bridge) watchNewWalletRegistered( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event NewWalletRegistered failed "+ + "subscription to event P2TRFraudRouterSet failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -9946,28 +12839,24 @@ func (b *Bridge) watchNewWalletRegistered( ) } -func (b *Bridge) PastNewWalletRegisteredEvents( +func (b *Bridge) PastP2TRFraudRouterSetEvents( startBlock uint64, endBlock *uint64, - ecdsaWalletIDFilter [][32]byte, - walletPubKeyHashFilter [][20]byte, -) ([]*abi.BridgeNewWalletRegistered, error) { - iterator, err := b.contract.FilterNewWalletRegistered( +) ([]*abi.BridgeP2TRFraudRouterSet, error) { + iterator, err := b.contract.FilterP2TRFraudRouterSet( &bind.FilterOpts{ Start: startBlock, End: endBlock, }, - ecdsaWalletIDFilter, - walletPubKeyHashFilter, ) if err != nil { return nil, fmt.Errorf( - "error retrieving past NewWalletRegistered events: [%v]", + "error retrieving past P2TRFraudRouterSet events: [%v]", err, ) } - events := make([]*abi.BridgeNewWalletRegistered, 0) + events := make([]*abi.BridgeP2TRFraudRouterSet, 0) for iterator.Next() { event := iterator.Event @@ -9977,9 +12866,9 @@ func (b *Bridge) PastNewWalletRegisteredEvents( return events, nil } -func (b *Bridge) NewWalletRequestedEvent( +func (b *Bridge) RebateStakingSetEvent( opts *ethereum.SubscribeOpts, -) *BNewWalletRequestedSubscription { +) *BRebateStakingSetSubscription { if opts == nil { opts = new(ethereum.SubscribeOpts) } @@ -9990,25 +12879,26 @@ func (b *Bridge) NewWalletRequestedEvent( opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks } - return &BNewWalletRequestedSubscription{ + return &BRebateStakingSetSubscription{ b, opts, } } -type BNewWalletRequestedSubscription struct { +type BRebateStakingSetSubscription struct { contract *Bridge opts *ethereum.SubscribeOpts } -type bridgeNewWalletRequestedFunc func( +type bridgeRebateStakingSetFunc func( + RebateStaking common.Address, blockNumber uint64, ) -func (nwrs *BNewWalletRequestedSubscription) OnEvent( - handler bridgeNewWalletRequestedFunc, +func (rsss *BRebateStakingSetSubscription) OnEvent( + handler bridgeRebateStakingSetFunc, ) subscription.EventSubscription { - eventChan := make(chan *abi.BridgeNewWalletRequested) + eventChan := make(chan *abi.BridgeRebateStakingSet) ctx, cancelCtx := context.WithCancel(context.Background()) go func() { @@ -10018,46 +12908,47 @@ func (nwrs *BNewWalletRequestedSubscription) OnEvent( return case event := <-eventChan: handler( + event.RebateStaking, event.Raw.BlockNumber, ) } } }() - sub := nwrs.Pipe(eventChan) + sub := rsss.Pipe(eventChan) return subscription.NewEventSubscription(func() { sub.Unsubscribe() cancelCtx() }) } -func (nwrs *BNewWalletRequestedSubscription) Pipe( - sink chan *abi.BridgeNewWalletRequested, +func (rsss *BRebateStakingSetSubscription) Pipe( + sink chan *abi.BridgeRebateStakingSet, ) subscription.EventSubscription { ctx, cancelCtx := context.WithCancel(context.Background()) go func() { - ticker := time.NewTicker(nwrs.opts.Tick) + ticker := time.NewTicker(rsss.opts.Tick) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: - lastBlock, err := nwrs.contract.blockCounter.CurrentBlock() + lastBlock, err := rsss.contract.blockCounter.CurrentBlock() if err != nil { bLogger.Errorf( "subscription failed to pull events: [%v]", err, ) } - fromBlock := lastBlock - nwrs.opts.PastBlocks + fromBlock := lastBlock - rsss.opts.PastBlocks bLogger.Infof( - "subscription monitoring fetching past NewWalletRequested events "+ + "subscription monitoring fetching past RebateStakingSet events "+ "starting from block [%v]", fromBlock, ) - events, err := nwrs.contract.PastNewWalletRequestedEvents( + events, err := rsss.contract.PastRebateStakingSetEvents( fromBlock, nil, ) @@ -10069,7 +12960,7 @@ func (nwrs *BNewWalletRequestedSubscription) Pipe( continue } bLogger.Infof( - "subscription monitoring fetched [%v] past NewWalletRequested events", + "subscription monitoring fetched [%v] past RebateStakingSet events", len(events), ) @@ -10080,7 +12971,7 @@ func (nwrs *BNewWalletRequestedSubscription) Pipe( } }() - sub := nwrs.contract.watchNewWalletRequested( + sub := rsss.contract.watchRebateStakingSet( sink, ) @@ -10090,11 +12981,11 @@ func (nwrs *BNewWalletRequestedSubscription) Pipe( }) } -func (b *Bridge) watchNewWalletRequested( - sink chan *abi.BridgeNewWalletRequested, +func (b *Bridge) watchRebateStakingSet( + sink chan *abi.BridgeRebateStakingSet, ) event.Subscription { subscribeFn := func(ctx context.Context) (event.Subscription, error) { - return b.contract.WatchNewWalletRequested( + return b.contract.WatchRebateStakingSet( &bind.WatchOpts{Context: ctx}, sink, ) @@ -10102,7 +12993,7 @@ func (b *Bridge) watchNewWalletRequested( thresholdViolatedFn := func(elapsed time.Duration) { bLogger.Warnf( - "subscription to event NewWalletRequested had to be "+ + "subscription to event RebateStakingSet had to be "+ "retried [%s] since the last attempt; please inspect "+ "host chain connectivity", elapsed, @@ -10111,7 +13002,7 @@ func (b *Bridge) watchNewWalletRequested( subscriptionFailedFn := func(err error) { bLogger.Errorf( - "subscription to event NewWalletRequested failed "+ + "subscription to event RebateStakingSet failed "+ "with error: [%v]; resubscription attempt will be "+ "performed", err, @@ -10127,11 +13018,11 @@ func (b *Bridge) watchNewWalletRequested( ) } -func (b *Bridge) PastNewWalletRequestedEvents( +func (b *Bridge) PastRebateStakingSetEvents( startBlock uint64, endBlock *uint64, -) ([]*abi.BridgeNewWalletRequested, error) { - iterator, err := b.contract.FilterNewWalletRequested( +) ([]*abi.BridgeRebateStakingSet, error) { + iterator, err := b.contract.FilterRebateStakingSet( &bind.FilterOpts{ Start: startBlock, End: endBlock, @@ -10139,12 +13030,12 @@ func (b *Bridge) PastNewWalletRequestedEvents( ) if err != nil { return nil, fmt.Errorf( - "error retrieving past NewWalletRequested events: [%v]", + "error retrieving past RebateStakingSet events: [%v]", err, ) } - events := make([]*abi.BridgeNewWalletRequested, 0) + events := make([]*abi.BridgeRebateStakingSet, 0) for iterator.Next() { event := iterator.Event @@ -11301,6 +14192,223 @@ func (b *Bridge) PastSpvMaintainerStatusUpdatedEvents( return events, nil } +func (b *Bridge) TaprootDepositRevealedEvent( + opts *ethereum.SubscribeOpts, + depositorFilter []common.Address, + walletPubKeyHashFilter [][20]byte, +) *BTaprootDepositRevealedSubscription { + if opts == nil { + opts = new(ethereum.SubscribeOpts) + } + if opts.Tick == 0 { + opts.Tick = chainutil.DefaultSubscribeOptsTick + } + if opts.PastBlocks == 0 { + opts.PastBlocks = chainutil.DefaultSubscribeOptsPastBlocks + } + + return &BTaprootDepositRevealedSubscription{ + b, + opts, + depositorFilter, + walletPubKeyHashFilter, + } +} + +type BTaprootDepositRevealedSubscription struct { + contract *Bridge + opts *ethereum.SubscribeOpts + depositorFilter []common.Address + walletPubKeyHashFilter [][20]byte +} + +type bridgeTaprootDepositRevealedFunc func( + FundingTxHash [32]byte, + FundingOutputIndex uint32, + Depositor common.Address, + Amount uint64, + BlindingFactor [8]byte, + WalletPubKeyHash [20]byte, + WalletXOnlyPublicKey [32]byte, + RefundPubKeyHash [20]byte, + RefundXOnlyPublicKey [32]byte, + RefundLocktime [4]byte, + Vault common.Address, + blockNumber uint64, +) + +func (tdrs *BTaprootDepositRevealedSubscription) OnEvent( + handler bridgeTaprootDepositRevealedFunc, +) subscription.EventSubscription { + eventChan := make(chan *abi.BridgeTaprootDepositRevealed) + ctx, cancelCtx := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case event := <-eventChan: + handler( + event.FundingTxHash, + event.FundingOutputIndex, + event.Depositor, + event.Amount, + event.BlindingFactor, + event.WalletPubKeyHash, + event.WalletXOnlyPublicKey, + event.RefundPubKeyHash, + event.RefundXOnlyPublicKey, + event.RefundLocktime, + event.Vault, + event.Raw.BlockNumber, + ) + } + } + }() + + sub := tdrs.Pipe(eventChan) + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (tdrs *BTaprootDepositRevealedSubscription) Pipe( + sink chan *abi.BridgeTaprootDepositRevealed, +) subscription.EventSubscription { + ctx, cancelCtx := context.WithCancel(context.Background()) + go func() { + ticker := time.NewTicker(tdrs.opts.Tick) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lastBlock, err := tdrs.contract.blockCounter.CurrentBlock() + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + } + fromBlock := lastBlock - tdrs.opts.PastBlocks + + bLogger.Infof( + "subscription monitoring fetching past TaprootDepositRevealed events "+ + "starting from block [%v]", + fromBlock, + ) + events, err := tdrs.contract.PastTaprootDepositRevealedEvents( + fromBlock, + nil, + tdrs.depositorFilter, + tdrs.walletPubKeyHashFilter, + ) + if err != nil { + bLogger.Errorf( + "subscription failed to pull events: [%v]", + err, + ) + continue + } + bLogger.Infof( + "subscription monitoring fetched [%v] past TaprootDepositRevealed events", + len(events), + ) + + for _, event := range events { + sink <- event + } + } + } + }() + + sub := tdrs.contract.watchTaprootDepositRevealed( + sink, + tdrs.depositorFilter, + tdrs.walletPubKeyHashFilter, + ) + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +func (b *Bridge) watchTaprootDepositRevealed( + sink chan *abi.BridgeTaprootDepositRevealed, + depositorFilter []common.Address, + walletPubKeyHashFilter [][20]byte, +) event.Subscription { + subscribeFn := func(ctx context.Context) (event.Subscription, error) { + return b.contract.WatchTaprootDepositRevealed( + &bind.WatchOpts{Context: ctx}, + sink, + depositorFilter, + walletPubKeyHashFilter, + ) + } + + thresholdViolatedFn := func(elapsed time.Duration) { + bLogger.Warnf( + "subscription to event TaprootDepositRevealed had to be "+ + "retried [%s] since the last attempt; please inspect "+ + "host chain connectivity", + elapsed, + ) + } + + subscriptionFailedFn := func(err error) { + bLogger.Errorf( + "subscription to event TaprootDepositRevealed failed "+ + "with error: [%v]; resubscription attempt will be "+ + "performed", + err, + ) + } + + return chainutil.WithResubscription( + chainutil.SubscriptionBackoffMax, + subscribeFn, + chainutil.SubscriptionAlertThreshold, + thresholdViolatedFn, + subscriptionFailedFn, + ) +} + +func (b *Bridge) PastTaprootDepositRevealedEvents( + startBlock uint64, + endBlock *uint64, + depositorFilter []common.Address, + walletPubKeyHashFilter [][20]byte, +) ([]*abi.BridgeTaprootDepositRevealed, error) { + iterator, err := b.contract.FilterTaprootDepositRevealed( + &bind.FilterOpts{ + Start: startBlock, + End: endBlock, + }, + depositorFilter, + walletPubKeyHashFilter, + ) + if err != nil { + return nil, fmt.Errorf( + "error retrieving past TaprootDepositRevealed events: [%v]", + err, + ) + } + + events := make([]*abi.BridgeTaprootDepositRevealed, 0) + + for iterator.Next() { + event := iterator.Event + events = append(events, event) + } + + return events, nil +} + func (b *Bridge) TreasuryUpdatedEvent( opts *ethereum.SubscribeOpts, ) *BTreasuryUpdatedSubscription { diff --git a/pkg/chain/ethereum/tbtc/gen/contract/WalletProposalValidator.go b/pkg/chain/ethereum/tbtc/gen/contract/WalletProposalValidator.go index b5d3591e01..2780682aac 100644 --- a/pkg/chain/ethereum/tbtc/gen/contract/WalletProposalValidator.go +++ b/pkg/chain/ethereum/tbtc/gen/contract/WalletProposalValidator.go @@ -585,4 +585,52 @@ func (wpv *WalletProposalValidator) ValidateRedemptionProposalAtBlock( return result, err } +func (wpv *WalletProposalValidator) ValidateTaprootDepositSweepProposal( + arg_proposal abi.WalletProposalValidatorDepositSweepProposal, + arg_depositsExtraInfo []abi.WalletProposalValidatorTaprootDepositExtraInfo, +) (bool, error) { + result, err := wpv.contract.ValidateTaprootDepositSweepProposal( + wpv.callerOptions, + arg_proposal, + arg_depositsExtraInfo, + ) + + if err != nil { + return result, wpv.errorResolver.ResolveError( + err, + wpv.callerOptions.From, + nil, + "validateTaprootDepositSweepProposal", + arg_proposal, + arg_depositsExtraInfo, + ) + } + + return result, err +} + +func (wpv *WalletProposalValidator) ValidateTaprootDepositSweepProposalAtBlock( + arg_proposal abi.WalletProposalValidatorDepositSweepProposal, + arg_depositsExtraInfo []abi.WalletProposalValidatorTaprootDepositExtraInfo, + blockNumber *big.Int, +) (bool, error) { + var result bool + + err := chainutil.CallAtBlock( + wpv.callerOptions.From, + blockNumber, + nil, + wpv.contractABI, + wpv.caller, + wpv.errorResolver, + wpv.contractAddress, + "validateTaprootDepositSweepProposal", + &result, + arg_proposal, + arg_depositsExtraInfo, + ) + + return result, err +} + // ------ Events ------- diff --git a/pkg/chain/ethereum/tbtc_test.go b/pkg/chain/ethereum/tbtc_test.go index 1c9eef1be0..206c7dead1 100644 --- a/pkg/chain/ethereum/tbtc_test.go +++ b/pkg/chain/ethereum/tbtc_test.go @@ -4,12 +4,19 @@ 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" + ecdsacontract "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen/contract" + frostabi "github.com/keep-network/keep-core/pkg/chain/ethereum/frost/gen/abi" + 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 +330,637 @@ func TestCalculateWalletID(t *testing.T) { testutils.AssertBytesEqual(t, expectedWalletID[:], actualWalletID[:]) } +func TestTbtcChainHasFrostAuthorization(t *testing.T) { + tests := map[string]struct { + chain *TbtcChain + expectedResult bool + }{ + "no frost contracts": { + chain: &TbtcChain{}, + expectedResult: false, + }, + "registry only": { + chain: &TbtcChain{ + frostWalletRegistry: &frostabi.FrostWalletRegistry{}, + }, + expectedResult: false, + }, + "sortition pool only": { + chain: &TbtcChain{ + frostSortitionPool: &ecdsacontract.EcdsaSortitionPool{}, + }, + expectedResult: false, + }, + "registry and sortition pool": { + chain: &TbtcChain{ + frostWalletRegistry: &frostabi.FrostWalletRegistry{}, + frostSortitionPool: &ecdsacontract.EcdsaSortitionPool{}, + }, + expectedResult: true, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + actualResult := test.chain.hasFrostAuthorization() + if actualResult != test.expectedResult { + t.Fatalf( + "unexpected FROST authorization result\nexpected: [%v]\nactual: [%v]", + test.expectedResult, + actualResult, + ) + } + }) + } +} + +type operatorIDResolverMock struct { + expectedOperator common.Address + operatorID chain.OperatorID + err error + called bool +} + +func (oirm *operatorIDResolverMock) GetOperatorID( + operator common.Address, +) (chain.OperatorID, error) { + oirm.called = true + + if operator != oirm.expectedOperator { + return 0, fmt.Errorf( + "unexpected operator address\nexpected: [%v]\nactual: [%v]", + oirm.expectedOperator, + operator, + ) + } + + return oirm.operatorID, oirm.err +} + +func TestGetOperatorIDUsesProvidedResolver(t *testing.T) { + expectedOperator := common.HexToAddress( + "0x7777777777777777777777777777777777777777", + ) + expectedOperatorID := chain.OperatorID(777) + + resolver := &operatorIDResolverMock{ + expectedOperator: expectedOperator, + operatorID: expectedOperatorID, + } + + actualOperatorID, err := getOperatorID(resolver, expectedOperator) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if !resolver.called { + t.Fatal("expected operator ID resolver to be called") + } + + if actualOperatorID != expectedOperatorID { + t.Fatalf( + "unexpected operator ID\nexpected: [%v]\nactual: [%v]", + expectedOperatorID, + actualOperatorID, + ) + } +} + +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) + + 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, + &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) + } + + // 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, + &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 + 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, + &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 + 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 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} + expectedWalletPublicKeyHash := [20]byte{0xaa} + + actualWalletPublicKeyHash, err := resolveWalletPublicKeyHashForWalletID( + walletID, + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: 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, + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: 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, + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: 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, + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: 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, + &walletPublicKeyHashForWalletIDBridgeMock{ + resolve: 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 { diff --git a/pkg/clientinfo/performance.go b/pkg/clientinfo/performance.go index 219216c22d..3e57f6a801 100644 --- a/pkg/clientinfo/performance.go +++ b/pkg/clientinfo/performance.go @@ -113,6 +113,7 @@ func (pm *PerformanceMetrics) registerAllMetrics() { MetricSigningSuccessTotal, MetricSigningFailedTotal, MetricSigningTimeoutsTotal, + MetricSigningNativeTBTCSignerFallbackTotal, MetricRedemptionExecutionsTotal, MetricRedemptionExecutionsSuccessTotal, MetricRedemptionExecutionsFailedTotal, @@ -170,43 +171,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.countersMutex.Lock() - pm.counters[name] = &counter{value: 0} - pm.countersMutex.Unlock() - 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() + pm.histogramsMutex.Lock() + for _, durationName := range perActionDurations { pm.histograms[durationName] = &histogram{ buckets: make(map[float64]float64), } - pm.histogramsMutex.Unlock() + } + pm.histogramsMutex.Unlock() + + for _, durationName := range perActionDurations { durationMetricName := durationName // Capture for closure pm.registry.ObserveApplicationSource( "performance", @@ -616,6 +633,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" // Redemption Metrics MetricRedemptionExecutionsTotal = "redemption_executions_total" diff --git a/pkg/clientinfo/performance_test.go b/pkg/clientinfo/performance_test.go index 75190c8545..e80817da6c 100644 --- a/pkg/clientinfo/performance_test.go +++ b/pkg/clientinfo/performance_test.go @@ -327,6 +327,7 @@ func TestMetricsInitialization(t *testing.T) { MetricDKGJoinedTotal, MetricSigningOperationsTotal, MetricSigningSuccessTotal, + MetricSigningNativeTBTCSignerFallbackTotal, } for _, counterName := range counters { diff --git a/pkg/frost/registry/dkg_result.go b/pkg/frost/registry/dkg_result.go new file mode 100644 index 0000000000..800e851de2 --- /dev/null +++ b/pkg/frost/registry/dkg_result.go @@ -0,0 +1,259 @@ +package registry + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/keep-network/keep-core/pkg/frost" +) + +const ( + // ResultDigestVersion is the literal version tag used by + // FrostDkgValidator.resultDigest. + ResultDigestVersion = "tbtc-frost-dkg-result-v1" +) + +var ( + uint32ArrayType = mustABIType("uint32[]") + uint8ArrayType = mustABIType("uint8[]") + uint256Type = mustABIType("uint256") + addressType = mustABIType("address") + bytes32Type = mustABIType("bytes32") + stringType = mustABIType("string") +) + +// FullMembers is the full selected group returned by the FROST sortition pool. +// +// This slice is used in the v4 digest and submitted result. Do not filter +// misbehaved members before using it for those purposes. +type FullMembers []uint32 + +// ActiveMembers is the filtered group after excluding 1-based misbehaved +// member indices. This slice is used for the submitted membersHash only. +type ActiveMembers []uint32 + +// MisbehavedMemberIndices holds sorted, unique, 1-based indices into +// FullMembers. +type MisbehavedMemberIndices []uint8 + +// Result contains the FROST DKG result fields submitted to FrostWalletRegistry. +type Result struct { + SubmitterMemberIndex uint64 + XOnlyOutputKey frost.OutputKey + MembersHash [32]byte + MisbehavedMembersIndices MisbehavedMemberIndices + Signatures []byte + SigningMembersIndices []uint64 + Members FullMembers +} + +// ActiveMembersFromMisbehaved returns the filtered active set used to compute +// Result.MembersHash. +func ActiveMembersFromMisbehaved( + members FullMembers, + misbehaved MisbehavedMemberIndices, +) (ActiveMembers, error) { + if err := validateMisbehavedMemberIndices(len(members), misbehaved); err != nil { + return nil, err + } + + if len(misbehaved) == 0 { + active := make(ActiveMembers, len(members)) + copy(active, members) + return active, nil + } + + active := make(ActiveMembers, 0, len(members)-len(misbehaved)) + misbehavedCursor := 0 + for i, member := range members { + memberIndex := uint8(i + 1) + if misbehavedCursor < len(misbehaved) && + memberIndex == misbehaved[misbehavedCursor] { + misbehavedCursor++ + continue + } + + active = append(active, member) + } + + return active, nil +} + +// AssembleResult builds a Result while keeping full and active member sets +// distinct. The full members list is persisted in the result and signed in the +// v4 digest; the filtered active members list is hashed into membersHash. +func AssembleResult( + submitterMemberIndex uint64, + xOnlyOutputKey frost.OutputKey, + members FullMembers, + misbehaved MisbehavedMemberIndices, + signatures []byte, + signingMembersIndices []uint64, +) (*Result, error) { + activeMembers, err := ActiveMembersFromMisbehaved(members, misbehaved) + if err != nil { + return nil, err + } + + membersHash, err := ActiveMembersHash(activeMembers) + if err != nil { + return nil, err + } + + result := &Result{ + SubmitterMemberIndex: submitterMemberIndex, + XOnlyOutputKey: xOnlyOutputKey, + MembersHash: membersHash, + MisbehavedMembersIndices: append(MisbehavedMemberIndices{}, misbehaved...), + Signatures: append([]byte{}, signatures...), + SigningMembersIndices: append([]uint64{}, signingMembersIndices...), + Members: append(FullMembers{}, members...), + } + + return result, nil +} + +// FullMembersHash returns keccak256(abi.encode(uint32[])). +func FullMembersHash(members FullMembers) ([32]byte, error) { + return uint32ArrayHash([]uint32(members)) +} + +// ActiveMembersHash returns keccak256(abi.encode(uint32[])). +func ActiveMembersHash(members ActiveMembers) ([32]byte, error) { + return uint32ArrayHash([]uint32(members)) +} + +// MisbehavedMembersHash returns keccak256(abi.encode(uint8[])). +func MisbehavedMembersHash( + misbehaved MisbehavedMemberIndices, +) ([32]byte, error) { + args := abi.Arguments{{Type: uint8ArrayType}} + encoded, err := args.Pack([]uint8(misbehaved)) + if err != nil { + return [32]byte{}, err + } + + return crypto.Keccak256Hash(encoded), nil +} + +// ResultDigest computes the pre-EIP-191 v4 digest expected by +// FrostDkgValidator.resultDigest. +func ResultDigest( + chainID *big.Int, + bridge common.Address, + registry common.Address, + seed *big.Int, + xOnlyOutputKey frost.OutputKey, + members FullMembers, + misbehaved MisbehavedMemberIndices, +) ([32]byte, error) { + if chainID == nil { + return [32]byte{}, fmt.Errorf("chain ID is nil") + } + if bridge == (common.Address{}) { + return [32]byte{}, fmt.Errorf("bridge address is zero") + } + if registry == (common.Address{}) { + return [32]byte{}, fmt.Errorf("registry address is zero") + } + if seed == nil { + return [32]byte{}, fmt.Errorf("seed is nil") + } + + fullMembersHash, err := FullMembersHash(members) + if err != nil { + return [32]byte{}, err + } + + misbehavedHash, err := MisbehavedMembersHash(misbehaved) + if err != nil { + return [32]byte{}, err + } + + args := abi.Arguments{ + {Type: stringType}, + {Type: uint256Type}, + {Type: addressType}, + {Type: addressType}, + {Type: uint256Type}, + {Type: bytes32Type}, + {Type: bytes32Type}, + {Type: bytes32Type}, + } + + encoded, err := args.Pack( + ResultDigestVersion, + chainID, + bridge, + registry, + seed, + [32]byte(xOnlyOutputKey), + fullMembersHash, + misbehavedHash, + ) + if err != nil { + return [32]byte{}, err + } + + return crypto.Keccak256Hash(encoded), nil +} + +// EthereumSignedMessageHash returns the go-ethereum personal-sign hash: +// keccak256("\x19Ethereum Signed Message:\n32" || digest). +func EthereumSignedMessageHash(digest [32]byte) [32]byte { + prefixed := make([]byte, 0, 28+len(digest)) + prefixed = append(prefixed, []byte("\x19Ethereum Signed Message:\n32")...) + prefixed = append(prefixed, digest[:]...) + + return crypto.Keccak256Hash(prefixed) +} + +func uint32ArrayHash(members []uint32) ([32]byte, error) { + args := abi.Arguments{{Type: uint32ArrayType}} + encoded, err := args.Pack(members) + if err != nil { + return [32]byte{}, err + } + + return crypto.Keccak256Hash(encoded), nil +} + +func validateMisbehavedMemberIndices( + groupSize int, + misbehaved MisbehavedMemberIndices, +) error { + if groupSize > 255 { + return fmt.Errorf("group size [%d] exceeds uint8 member index capacity", groupSize) + } + + var previous uint8 + for i, memberIndex := range misbehaved { + if memberIndex == 0 || int(memberIndex) > groupSize { + return fmt.Errorf( + "misbehaved member index [%d] out of range [1, %d]", + memberIndex, + groupSize, + ) + } + + if i > 0 && memberIndex <= previous { + return fmt.Errorf("misbehaved member indices must be sorted and unique") + } + + previous = memberIndex + } + + return nil +} + +func mustABIType(name string) abi.Type { + t, err := abi.NewType(name, "", nil) + if err != nil { + panic(err) + } + + return t +} diff --git a/pkg/frost/registry/dkg_result_test.go b/pkg/frost/registry/dkg_result_test.go new file mode 100644 index 0000000000..95ade636e5 --- /dev/null +++ b/pkg/frost/registry/dkg_result_test.go @@ -0,0 +1,350 @@ +package registry + +import ( + "encoding/hex" + "encoding/json" + "math/big" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/keep-network/keep-core/pkg/frost" +) + +const ( + v4DigestFixtureTestPath = "testdata/v4_digest_fixture.json" + v4DigestFixtureRepoPath = "pkg/frost/registry/testdata/v4_digest_fixture.json" +) + +type v4DigestFixture struct { + Name string `json:"name"` + Version string `json:"version"` + ChainID string `json:"chainID"` + Bridge string `json:"bridge"` + Registry string `json:"registry"` + Seed string `json:"seed"` + XOnlyOutputKey string `json:"xOnlyOutputKey"` + Members []uint32 `json:"members"` + MisbehavedMembersIndices []uint8 `json:"misbehavedMembersIndices"` + FullMembersHash string `json:"fullMembersHash"` + ActiveMembersHash string `json:"activeMembersHash"` + Digest string `json:"digest"` + EthereumSignedMessageHash string `json:"ethereumSignedMessageHash"` + Generator struct { + Source string `json:"source"` + Command string `json:"command"` + } `json:"generator"` + DriftCheck struct { + TbtcPath string `json:"tbtc_path"` + KeepCorePath string `json:"keep_core_path"` + Rule string `json:"rule"` + } `json:"drift_check"` +} + +func TestResultDigestMatchesCrossRepoFixture(t *testing.T) { + fixture := loadV4DigestFixture(t) + + chainID := mustBigInt(t, fixture.ChainID) + seed := mustBigInt(t, fixture.Seed) + digest, err := ResultDigest( + chainID, + common.HexToAddress(fixture.Bridge), + common.HexToAddress(fixture.Registry), + seed, + mustOutputKey(t, fixture.XOnlyOutputKey), + FullMembers(fixture.Members), + MisbehavedMemberIndices(fixture.MisbehavedMembersIndices), + ) + if err != nil { + t.Fatalf("unexpected digest error: [%v]", err) + } + + // Fixture generated with the TS reference shape: + // keccak256(defaultAbiCoder.encode( + // ["string","uint256","address","address","uint256","bytes32","bytes32","bytes32"], + // ["tbtc-frost-dkg-result-v1", chainID, bridge, registry, seed, + // xOnlyOutputKey, keccak256(abi.encode(uint32[] members)), + // keccak256(abi.encode(uint8[] misbehavedMembersIndices))])) + expectedDigest := mustBytes32(t, fixture.Digest) + + if digest != expectedDigest { + t.Fatalf( + "unexpected digest\nexpected: [0x%x]\nactual: [0x%x]", + expectedDigest, + digest, + ) + } +} + +func TestResultDigestRejectsZeroBindingAddresses(t *testing.T) { + fixture := loadV4DigestFixture(t) + chainID := mustBigInt(t, fixture.ChainID) + seed := mustBigInt(t, fixture.Seed) + outputKey := mustOutputKey(t, fixture.XOnlyOutputKey) + + _, err := ResultDigest( + chainID, + common.Address{}, + common.HexToAddress(fixture.Registry), + seed, + outputKey, + FullMembers(fixture.Members), + MisbehavedMemberIndices(fixture.MisbehavedMembersIndices), + ) + if err == nil || !strings.Contains(err.Error(), "bridge address is zero") { + t.Fatalf("expected zero bridge rejection, got [%v]", err) + } + + _, err = ResultDigest( + chainID, + common.HexToAddress(fixture.Bridge), + common.Address{}, + seed, + outputKey, + FullMembers(fixture.Members), + MisbehavedMemberIndices(fixture.MisbehavedMembersIndices), + ) + if err == nil || !strings.Contains(err.Error(), "registry address is zero") { + t.Fatalf("expected zero registry rejection, got [%v]", err) + } +} + +func TestMembersHashesKeepFullAndActiveSetsDistinct(t *testing.T) { + fixture := loadV4DigestFixture(t) + fullMembers := FullMembers(fixture.Members) + misbehaved := MisbehavedMemberIndices(fixture.MisbehavedMembersIndices) + + activeMembers, err := ActiveMembersFromMisbehaved(fullMembers, misbehaved) + if err != nil { + t.Fatalf("unexpected active members error: [%v]", err) + } + + expectedActiveMembers := ActiveMembers{101, 303, 404} + if len(activeMembers) != len(expectedActiveMembers) { + t.Fatalf( + "unexpected active members length\nexpected: [%d]\nactual: [%d]", + len(expectedActiveMembers), + len(activeMembers), + ) + } + for i := range expectedActiveMembers { + if activeMembers[i] != expectedActiveMembers[i] { + t.Fatalf( + "unexpected active member at index [%d]\nexpected: [%d]\nactual: [%d]", + i, + expectedActiveMembers[i], + activeMembers[i], + ) + } + } + + fullHash, err := FullMembersHash(fullMembers) + if err != nil { + t.Fatalf("unexpected full members hash error: [%v]", err) + } + + activeHash, err := ActiveMembersHash(activeMembers) + if err != nil { + t.Fatalf("unexpected active members hash error: [%v]", err) + } + + expectedFullHash := mustBytes32(t, fixture.FullMembersHash) + expectedActiveHash := mustBytes32(t, fixture.ActiveMembersHash) + + if fullHash != expectedFullHash { + t.Fatalf( + "unexpected full members hash\nexpected: [0x%x]\nactual: [0x%x]", + expectedFullHash, + fullHash, + ) + } + if activeHash != expectedActiveHash { + t.Fatalf( + "unexpected active members hash\nexpected: [0x%x]\nactual: [0x%x]", + expectedActiveHash, + activeHash, + ) + } + if fullHash == activeHash { + t.Fatal("expected full and active members hashes to differ") + } +} + +func TestAssembleResultUsesFilteredMembersHash(t *testing.T) { + fixture := loadV4DigestFixture(t) + + result, err := AssembleResult( + 1, + mustOutputKey(t, fixture.XOnlyOutputKey), + FullMembers(fixture.Members), + MisbehavedMemberIndices(fixture.MisbehavedMembersIndices), + []byte{0x01, 0x02}, + []uint64{1, 3, 4}, + ) + if err != nil { + t.Fatalf("unexpected assembly error: [%v]", err) + } + + expectedMembersHash := mustBytes32(t, fixture.ActiveMembersHash) + + if result.MembersHash != expectedMembersHash { + t.Fatalf( + "unexpected result membersHash\nexpected: [0x%x]\nactual: [0x%x]", + expectedMembersHash, + result.MembersHash, + ) + } +} + +func TestEthereumSignedMessageHash(t *testing.T) { + fixture := loadV4DigestFixture(t) + + hash := EthereumSignedMessageHash(mustBytes32(t, fixture.Digest)) + expected := mustBytes32(t, fixture.EthereumSignedMessageHash) + + if hash != expected { + t.Fatalf( + "unexpected EIP-191 hash\nexpected: [0x%x]\nactual: [0x%x]", + expected, + hash, + ) + } +} + +func TestV4DigestFixtureMetadata(t *testing.T) { + fixture := loadV4DigestFixture(t) + + if fixture.Name != "frost-dkg-result-digest-vectors" { + t.Errorf("unexpected fixture name: [%s]", fixture.Name) + } + if fixture.Version != "v1" { + t.Errorf("unexpected fixture version: [%s]", fixture.Version) + } + if fixture.Generator.Source != "tlabs-xyz/tbtc/contracts/tbtc-v2/test/integration/utils/frost-wallet-registry.ts:computeFrostResultDigest" { + t.Errorf("unexpected generator source: [%s]", fixture.Generator.Source) + } + if fixture.Generator.Command == "" { + t.Error("generator command must be documented") + } + if fixture.DriftCheck.TbtcPath != "docs/test-vectors/frost-dkg-result-digest-v1.json" { + t.Errorf("unexpected tbtc drift-check path: [%s]", fixture.DriftCheck.TbtcPath) + } + if fixture.DriftCheck.KeepCorePath != v4DigestFixtureRepoPath { + t.Errorf( + "unexpected keep-core drift-check path\nexpected: [%s]\nactual: [%s]", + v4DigestFixtureRepoPath, + fixture.DriftCheck.KeepCorePath, + ) + } + if fixture.DriftCheck.Rule == "" { + t.Error("drift-check rule must be documented") + } +} + +func TestV4DigestFixtureFileShouldExistAtMirrorPath(t *testing.T) { + fixture := loadV4DigestFixture(t) + + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller: cannot locate test source file") + } + + repoRoot := filepath.Clean( + filepath.Join(filepath.Dir(thisFile), "..", "..", ".."), + ) + abs := filepath.Join(repoRoot, fixture.DriftCheck.KeepCorePath) + if _, err := os.Stat(abs); err != nil { + t.Fatalf( + "fixture self-declares it lives at [%s] resolved to [%s] but the file is not there: [%v]", + fixture.DriftCheck.KeepCorePath, + abs, + err, + ) + } +} + +func TestActiveMembersFromMisbehavedRejectsInvalidIndices(t *testing.T) { + testCases := map[string]MisbehavedMemberIndices{ + "zero": {0}, + "too large": {4}, + "duplicate": {2, 2}, + "unsorted": {3, 1}, + } + + for name, misbehaved := range testCases { + t.Run(name, func(t *testing.T) { + _, err := ActiveMembersFromMisbehaved( + FullMembers{101, 202, 303}, + misbehaved, + ) + if err == nil { + t.Fatal("expected error") + } + }) + } +} + +func loadV4DigestFixture(t *testing.T) *v4DigestFixture { + t.Helper() + + data, err := os.ReadFile(v4DigestFixtureTestPath) + if err != nil { + t.Fatalf("cannot read fixture: [%v]", err) + } + + var fixture v4DigestFixture + if err := json.Unmarshal(data, &fixture); err != nil { + t.Fatalf("cannot unmarshal fixture: [%v]", err) + } + + return &fixture +} + +func mustBigInt(t *testing.T, value string) *big.Int { + t.Helper() + + result, ok := new(big.Int).SetString(value, 10) + if !ok { + t.Fatalf("cannot parse big int: [%s]", value) + } + + return result +} + +func mustOutputKey(t *testing.T, hexString string) frost.OutputKey { + t.Helper() + + var outputKey frost.OutputKey + copy(outputKey[:], mustBytes(t, hexString, frost.OutputKeySize)) + return outputKey +} + +func mustBytes32(t *testing.T, hexString string) [32]byte { + t.Helper() + + var result [32]byte + copy(result[:], mustBytes(t, hexString, 32)) + return result +} + +func mustBytes(t *testing.T, hexString string, expectedLength int) []byte { + t.Helper() + + decoded, err := hex.DecodeString(strings.TrimPrefix(hexString, "0x")) + if err != nil { + t.Fatalf("cannot decode hex string: [%v]", err) + } + + if len(decoded) != expectedLength { + t.Fatalf( + "unexpected decoded length\nexpected: [%d]\nactual: [%d]", + expectedLength, + len(decoded), + ) + } + + return decoded +} diff --git a/pkg/frost/registry/testdata/README.md b/pkg/frost/registry/testdata/README.md new file mode 100644 index 0000000000..b8a5c0f8a1 --- /dev/null +++ b/pkg/frost/registry/testdata/README.md @@ -0,0 +1,16 @@ +# FROST DKG Digest Fixture + +`v4_digest_fixture.json` pins the keep-core Go digest implementation against +the tBTC TypeScript reference in +`contracts/tbtc-v2/test/integration/utils/frost-wallet-registry.ts`. + +Regenerate the hash fields from the sibling `tlabs-xyz/tbtc` checkout: + +```sh +cd ../tbtc/contracts/tbtc-v2 +pnpm exec ts-node -e 'import hre from "hardhat"; import { computeFrostResultDigest } from "./test/integration/utils/frost-wallet-registry"; const { ethers } = hre; const members = [101,202,303,404,505]; const misbehavedMembersIndices = [2,5]; const activeMembers = members.filter((_, i) => !misbehavedMembersIndices.includes(i + 1)); const digest = computeFrostResultDigest(hre, { chainId: 31337, bridge: "0x1111111111111111111111111111111111111111", registry: "0x2222222222222222222222222222222222222222", seed: 123456789, xOnlyOutputKey: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", members, misbehavedMembersIndices }); console.log(JSON.stringify({ fullMembersHash: ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint32[]"], [members])), activeMembersHash: ethers.utils.keccak256(ethers.utils.defaultAbiCoder.encode(["uint32[]"], [activeMembers])), digest, ethereumSignedMessageHash: ethers.utils.hashMessage(ethers.utils.arrayify(digest)) }, null, 2));' +``` + +The fixture metadata also declares the intended tBTC mirror path. The paired +tBTC-side emitter should produce byte-for-byte equivalent hash values for the +same inputs. diff --git a/pkg/frost/registry/testdata/v4_digest_fixture.json b/pkg/frost/registry/testdata/v4_digest_fixture.json new file mode 100644 index 0000000000..18fcfcb926 --- /dev/null +++ b/pkg/frost/registry/testdata/v4_digest_fixture.json @@ -0,0 +1,25 @@ +{ + "name": "frost-dkg-result-digest-vectors", + "version": "v1", + "description": "FROST DKG resultDigest v4 fixture generated from the tBTC TypeScript reference flow. The tBTC bridge-side reference is contracts/tbtc-v2/test/integration/utils/frost-wallet-registry.ts:computeFrostResultDigest.", + "generator": { + "source": "tlabs-xyz/tbtc/contracts/tbtc-v2/test/integration/utils/frost-wallet-registry.ts:computeFrostResultDigest", + "command": "See pkg/frost/registry/testdata/README.md for the verified pnpm exec ts-node command." + }, + "chainID": "31337", + "bridge": "0x1111111111111111111111111111111111111111", + "registry": "0x2222222222222222222222222222222222222222", + "seed": "123456789", + "xOnlyOutputKey": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "members": [101, 202, 303, 404, 505], + "misbehavedMembersIndices": [2, 5], + "fullMembersHash": "0x048553822a9f886e64193ef9da3f71732a9708edb45cff48e68466e635f6dbf7", + "activeMembersHash": "0x4c78efa0361537bf6929c6e824b152e9b9be9140da28cbd9e1e569e483c4a16f", + "digest": "0x45c93e369c6e4b43cbeebf09c7c639526ea0b826d3e89d87c1cd137a08dfc1d9", + "ethereumSignedMessageHash": "0xd70747a38b3969e9d95700a9fc7eae13883f9ee960290d7aee8114623fb9d6c9", + "drift_check": { + "tbtc_path": "docs/test-vectors/frost-dkg-result-digest-v1.json", + "keep_core_path": "pkg/frost/registry/testdata/v4_digest_fixture.json", + "rule": "The tBTC-side test must produce the same digest, members hashes, and EIP-191 hash for these inputs using computeFrostResultDigest. If the digest format changes, update the tBTC emitter and this keep-core fixture together." + } +} diff --git a/pkg/frost/retry/retry.go b/pkg/frost/retry/retry.go new file mode 100644 index 0000000000..798d3bed30 --- /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[k] + + // 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..5e0a16dbcd --- /dev/null +++ b/pkg/frost/retry/retry_test.go @@ -0,0 +1,290 @@ +package retry + +import ( + "fmt" + "math/rand" + "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 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, + 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/attempt/attempt_context.go b/pkg/frost/roast/attempt/attempt_context.go new file mode 100644 index 0000000000..772ecdaa02 --- /dev/null +++ b/pkg/frost/roast/attempt/attempt_context.go @@ -0,0 +1,299 @@ +// Package attempt implements the AttemptContext type that binds every +// signing-protocol message to a deterministic, group-agreed context. +// +// This package is the Phase 1 deliverable from RFC-21 (ROAST Coordinator, +// Retry, and Transition Evidence). It introduces only the type, its +// deterministic seed derivation, and the canonical hash used to bind +// protocol messages to an attempt. No protocol behaviour changes in this +// phase; consumers are wired in later phases behind build tags. +package attempt + +import ( + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "sort" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// MessageDigestLength is the canonical byte length of a signing-message +// digest carried in AttemptContext. The protocol always uses SHA-256 +// digests of the BIP-340 tag-bound payload, so 32 bytes is correct for +// every signing flow this package is concerned with. +const MessageDigestLength = 32 + +// AttemptSeedLength is the canonical byte length of the per-attempt +// participant-shuffle seed. The seed is derived, never chosen -- +// see DeriveAttemptSeed. +const AttemptSeedLength = 32 + +// AttemptContext binds an in-flight ROAST signing attempt to a +// deterministic context. Every honest signer must construct the same +// AttemptContext for a given (session, key group, message, attempt +// number) and must reject any protocol message whose AttemptContextHash +// does not match the locally-computed context. +// +// AttemptContext fields are public so test fixtures can construct +// contexts directly, but production callers should use NewAttemptContext +// which validates inputs and derives the seed. +type AttemptContext struct { + // SessionID identifies the signing session at the keep-core layer. + // It is opaque to the ROAST coordinator; the coordinator only + // requires it to be stable across the session's attempts. + SessionID string + // KeyGroupID identifies the FROST key group whose threshold share + // will sign. It is opaque to the coordinator; equality across honest + // signers is required. + KeyGroupID string + // MessageDigest is the 32-byte SHA-256 digest of the BIP-340 + // tag-bound signing message. + MessageDigest [MessageDigestLength]byte + // AttemptNumber is the zero-based ordinal of this attempt within + // the session. Attempt 0 is the first attempt; later attempts are + // driven by NextAttempt in the coordinator state machine + // (introduced in later RFC-21 phases). + AttemptNumber uint32 + // IncludedSet is the set of member indices that are eligible to + // participate in this attempt. Must be sorted ascending. Must not + // be empty. + IncludedSet []group.MemberIndex + // ExcludedSet is the set of member indices permanently excluded + // from this attempt by the coordinator's transition-evidence + // policy. Must be sorted ascending. May be empty. Permanent + // exclusion follows from transport-blamable (overflow) or + // validation-blamable (non-transport reject) evidence, never + // from silence alone. + ExcludedSet []group.MemberIndex + // TransientlyParked is the set of member indices skipped from + // THIS attempt only because they were silent (deadline expiry) + // at the previous attempt. Parking is strictly transient: a + // peer is unparked at the attempt after the one that skipped + // them, so a falsely-silenced honest peer (network blip, + // coordinator censorship caught at VerifyBundle) is reinstated + // without intervention. Must be sorted ascending. May be empty. + TransientlyParked []group.MemberIndex + // AttemptSeed is derived from group-agreed inputs and binds the + // attempt to inputs that no coordinator can manipulate. See + // DeriveAttemptSeed. + AttemptSeed [AttemptSeedLength]byte +} + +// DeriveAttemptSeed computes the per-attempt seed from inputs the group +// already agrees on. The seed binds the attempt's participant selection +// to fixed session inputs so a coordinator cannot shape the shuffle by +// picking a favourable seed. +// +// The derivation is: +// +// AttemptSeed = SHA256( +// DkgGroupPublicKey || SessionID || MessageDigest, +// ) +// +// Where SessionID is encoded as the raw UTF-8 bytes (the canonical +// representation used elsewhere in keep-core) and the other inputs are +// raw bytes. +func DeriveAttemptSeed( + dkgGroupPublicKey []byte, + sessionID string, + messageDigest [MessageDigestLength]byte, +) [AttemptSeedLength]byte { + h := sha256.New() + h.Write(dkgGroupPublicKey) + h.Write([]byte(sessionID)) + h.Write(messageDigest[:]) + var out [AttemptSeedLength]byte + copy(out[:], h.Sum(nil)) + return out +} + +// NewAttemptContext constructs an AttemptContext with the seed derived +// from group-agreed inputs. The IncludedSet and ExcludedSet are sorted +// ascending in the returned context regardless of input order; honest +// signers therefore produce identical contexts from identical input +// values. +// +// Returns an error if the included set is empty, if any member appears +// in both sets, or if either set contains duplicates. +// +// This is the seven-argument convenience that initialises an attempt +// with no TransientlyParked entries (the attempt-zero shape). For +// later attempts produced by the coordinator's NextAttempt policy, +// use NewAttemptContextWithParking. +func NewAttemptContext( + sessionID string, + keyGroupID string, + dkgGroupPublicKey []byte, + messageDigest [MessageDigestLength]byte, + attemptNumber uint32, + includedSet []group.MemberIndex, + excludedSet []group.MemberIndex, +) (AttemptContext, error) { + return NewAttemptContextWithParking( + sessionID, + keyGroupID, + dkgGroupPublicKey, + messageDigest, + attemptNumber, + includedSet, + excludedSet, + nil, + ) +} + +// NewAttemptContextWithParking is the full constructor used by the +// coordinator's NextAttempt policy. It accepts a transientlyParked +// set in addition to the inputs of NewAttemptContext. +// +// Validation: included set non-empty; no duplicates in any set; +// included/excluded sets disjoint; included/parked sets disjoint; +// excluded/parked sets disjoint. +func NewAttemptContextWithParking( + sessionID string, + keyGroupID string, + dkgGroupPublicKey []byte, + messageDigest [MessageDigestLength]byte, + attemptNumber uint32, + includedSet []group.MemberIndex, + excludedSet []group.MemberIndex, + transientlyParked []group.MemberIndex, +) (AttemptContext, error) { + if len(includedSet) == 0 { + return AttemptContext{}, errors.New( + "attempt context: included set must not be empty", + ) + } + included, err := canonicalMemberSet(includedSet, "included") + if err != nil { + return AttemptContext{}, err + } + excluded, err := canonicalMemberSet(excludedSet, "excluded") + if err != nil { + return AttemptContext{}, err + } + parked, err := canonicalMemberSet(transientlyParked, "transiently parked") + if err != nil { + return AttemptContext{}, err + } + if hasOverlap(included, excluded) { + return AttemptContext{}, errors.New( + "attempt context: included and excluded sets overlap", + ) + } + if hasOverlap(included, parked) { + return AttemptContext{}, errors.New( + "attempt context: included and transiently-parked sets overlap", + ) + } + if hasOverlap(excluded, parked) { + return AttemptContext{}, errors.New( + "attempt context: excluded and transiently-parked sets overlap", + ) + } + return AttemptContext{ + SessionID: sessionID, + KeyGroupID: keyGroupID, + MessageDigest: messageDigest, + AttemptNumber: attemptNumber, + IncludedSet: included, + ExcludedSet: excluded, + TransientlyParked: parked, + AttemptSeed: DeriveAttemptSeed( + dkgGroupPublicKey, + sessionID, + messageDigest, + ), + }, nil +} + +// Hash returns the canonical 32-byte hash of the attempt context. The +// hash is the SHA-256 of a length-prefixed, sorted-set canonical +// encoding so any two honest signers that construct semantically equal +// AttemptContexts produce byte-identical hashes regardless of input +// ordering. +// +// The hash is the value carried in protocol messages as +// AttemptContextHash. A receiver that computes a different hash than +// the one carried by an inbound message must reject the message: it +// belongs to a different attempt. +func (c AttemptContext) Hash() [MessageDigestLength]byte { + h := sha256.New() + writeLenPrefixed(h, []byte(c.SessionID)) + writeLenPrefixed(h, []byte(c.KeyGroupID)) + h.Write(c.MessageDigest[:]) + var attemptNumberBuf [4]byte + binary.BigEndian.PutUint32(attemptNumberBuf[:], c.AttemptNumber) + h.Write(attemptNumberBuf[:]) + writeMemberSet(h, c.IncludedSet) + writeMemberSet(h, c.ExcludedSet) + writeMemberSet(h, c.TransientlyParked) + h.Write(c.AttemptSeed[:]) + var out [MessageDigestLength]byte + copy(out[:], h.Sum(nil)) + return out +} + +func canonicalMemberSet( + members []group.MemberIndex, + label string, +) ([]group.MemberIndex, error) { + if len(members) == 0 { + return []group.MemberIndex{}, nil + } + out := make([]group.MemberIndex, len(members)) + copy(out, members) + sort.Slice(out, func(i, j int) bool { + return out[i] < out[j] + }) + for i := 1; i < len(out); i++ { + if out[i] == out[i-1] { + return nil, fmt.Errorf( + "attempt context: %s set contains duplicate member [%d]", + label, + out[i], + ) + } + } + return out, nil +} + +func hasOverlap(a, b []group.MemberIndex) bool { + i, j := 0, 0 + for i < len(a) && j < len(b) { + switch { + case a[i] < b[j]: + i++ + case a[i] > b[j]: + j++ + default: + return true + } + } + return false +} + +// byteWriter is the subset of io.Writer the canonical-encoding helpers +// need. Hash.Write (the only production implementation) is documented to +// never return an error, so the helpers discard the (int, error) result +// explicitly to make that contract reader-visible (and to satisfy gosec +// G104). +type byteWriter interface { + Write(p []byte) (n int, err error) +} + +func writeLenPrefixed(w byteWriter, data []byte) { + var lenBuf [4]byte + binary.BigEndian.PutUint32(lenBuf[:], uint32(len(data))) + _, _ = w.Write(lenBuf[:]) + _, _ = w.Write(data) +} + +func writeMemberSet(w byteWriter, members []group.MemberIndex) { + var lenBuf [4]byte + binary.BigEndian.PutUint32(lenBuf[:], uint32(len(members))) + _, _ = w.Write(lenBuf[:]) + for _, m := range members { + _, _ = w.Write([]byte{byte(m)}) + } +} diff --git a/pkg/frost/roast/attempt/attempt_context_test.go b/pkg/frost/roast/attempt/attempt_context_test.go new file mode 100644 index 0000000000..13c5408a8c --- /dev/null +++ b/pkg/frost/roast/attempt/attempt_context_test.go @@ -0,0 +1,435 @@ +package attempt + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "strings" + "testing" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestDeriveAttemptSeed_IsPureFunctionOfInputs(t *testing.T) { + dkgPub := []byte{0x02, 0x01, 0x02, 0x03, 0x04} + sessionID := "session-a" + var digest [MessageDigestLength]byte + copy(digest[:], bytes.Repeat([]byte{0x42}, MessageDigestLength)) + + a := DeriveAttemptSeed(dkgPub, sessionID, digest) + b := DeriveAttemptSeed(dkgPub, sessionID, digest) + if a != b { + t.Fatalf("derivation not deterministic: %x != %x", a, b) + } + + expected := sha256.Sum256( + append(append(append([]byte{}, dkgPub...), []byte(sessionID)...), digest[:]...), + ) + if a != expected { + t.Fatalf( + "derivation does not match SHA256(dkgPub || sessionID || messageDigest): got %x want %x", + a, expected, + ) + } +} + +func TestDeriveAttemptSeed_SensitiveToEachInput(t *testing.T) { + base := DeriveAttemptSeed( + []byte{0x01, 0x02}, + "session-a", + [MessageDigestLength]byte{0x01}, + ) + + tests := []struct { + name string + dkgPub []byte + sessionID string + digest [MessageDigestLength]byte + }{ + { + name: "different DKG public key", + dkgPub: []byte{0x01, 0x03}, + sessionID: "session-a", + digest: [MessageDigestLength]byte{0x01}, + }, + { + name: "different session ID", + dkgPub: []byte{0x01, 0x02}, + sessionID: "session-b", + digest: [MessageDigestLength]byte{0x01}, + }, + { + name: "different message digest", + dkgPub: []byte{0x01, 0x02}, + sessionID: "session-a", + digest: [MessageDigestLength]byte{0x02}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DeriveAttemptSeed(tt.dkgPub, tt.sessionID, tt.digest) + if got == base { + t.Fatalf("seed collided with base for %s", tt.name) + } + }) + } +} + +func TestNewAttemptContext_SortsAndDeduplicates(t *testing.T) { + dkgPub := []byte{0x01} + digest := [MessageDigestLength]byte{0xaa} + + included := []group.MemberIndex{5, 3, 4, 1, 2} + excluded := []group.MemberIndex{7, 6} + + ctx, err := NewAttemptContext( + "session", "key-group", dkgPub, digest, 0, included, excluded, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + want := []group.MemberIndex{1, 2, 3, 4, 5} + if !memberSlicesEqual(ctx.IncludedSet, want) { + t.Fatalf( + "included set not sorted: got %v want %v", + ctx.IncludedSet, want, + ) + } + wantExcluded := []group.MemberIndex{6, 7} + if !memberSlicesEqual(ctx.ExcludedSet, wantExcluded) { + t.Fatalf( + "excluded set not sorted: got %v want %v", + ctx.ExcludedSet, wantExcluded, + ) + } + + if !bytes.Equal(included, []group.MemberIndex{5, 3, 4, 1, 2}) { + t.Fatalf( + "caller's included slice was mutated: %v", + included, + ) + } +} + +func TestNewAttemptContext_RejectsEmptyIncludedSet(t *testing.T) { + _, err := NewAttemptContext( + "session", "kg", []byte{0x01}, + [MessageDigestLength]byte{}, 0, + nil, nil, + ) + if err == nil { + t.Fatal("expected error for empty included set") + } + if !strings.Contains(err.Error(), "included set must not be empty") { + t.Fatalf("unexpected error message: %v", err) + } +} + +func TestNewAttemptContext_RejectsDuplicates(t *testing.T) { + tests := []struct { + name string + included []group.MemberIndex + excluded []group.MemberIndex + want string + }{ + { + name: "duplicate in included set", + included: []group.MemberIndex{1, 2, 2, 3}, + excluded: nil, + want: "included set contains duplicate", + }, + { + name: "duplicate in excluded set", + included: []group.MemberIndex{1, 2}, + excluded: []group.MemberIndex{4, 4}, + want: "excluded set contains duplicate", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewAttemptContext( + "session", "kg", []byte{0x01}, + [MessageDigestLength]byte{}, 0, + tt.included, tt.excluded, + ) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), tt.want) { + t.Fatalf( + "unexpected error message: got %q want substring %q", + err.Error(), tt.want, + ) + } + }) + } +} + +func TestNewAttemptContext_RejectsOverlap(t *testing.T) { + _, err := NewAttemptContext( + "session", "kg", []byte{0x01}, + [MessageDigestLength]byte{}, 0, + []group.MemberIndex{1, 2, 3}, + []group.MemberIndex{3, 4}, + ) + if err == nil { + t.Fatal("expected overlap error") + } + if !strings.Contains(err.Error(), "overlap") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestAttemptContextHash_IsDeterministicAcrossInputOrdering(t *testing.T) { + dkgPub := []byte{0xab, 0xcd} + digest := [MessageDigestLength]byte{0x77} + + ctxA, err := NewAttemptContext( + "session", "kg", dkgPub, digest, 7, + []group.MemberIndex{5, 3, 4, 1, 2}, + []group.MemberIndex{7, 6}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + ctxB, err := NewAttemptContext( + "session", "kg", dkgPub, digest, 7, + []group.MemberIndex{1, 2, 3, 4, 5}, + []group.MemberIndex{6, 7}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if ctxA.Hash() != ctxB.Hash() { + t.Fatalf( + "semantically equal contexts produced different hashes: %x vs %x", + ctxA.Hash(), ctxB.Hash(), + ) + } +} + +func TestAttemptContextHash_SensitiveToEachField(t *testing.T) { + base, err := NewAttemptContext( + "session", "kg", []byte{0x01}, + [MessageDigestLength]byte{0x05}, 3, + []group.MemberIndex{1, 2, 3}, + []group.MemberIndex{4}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + baseHash := base.Hash() + + type mutator struct { + name string + fn func() (AttemptContext, error) + } + mutators := []mutator{ + { + name: "different session ID", + fn: func() (AttemptContext, error) { + return NewAttemptContext( + "session-2", "kg", []byte{0x01}, + [MessageDigestLength]byte{0x05}, 3, + []group.MemberIndex{1, 2, 3}, + []group.MemberIndex{4}, + ) + }, + }, + { + name: "different key group ID", + fn: func() (AttemptContext, error) { + return NewAttemptContext( + "session", "kg-2", []byte{0x01}, + [MessageDigestLength]byte{0x05}, 3, + []group.MemberIndex{1, 2, 3}, + []group.MemberIndex{4}, + ) + }, + }, + { + name: "different message digest", + fn: func() (AttemptContext, error) { + return NewAttemptContext( + "session", "kg", []byte{0x01}, + [MessageDigestLength]byte{0x06}, 3, + []group.MemberIndex{1, 2, 3}, + []group.MemberIndex{4}, + ) + }, + }, + { + name: "different attempt number", + fn: func() (AttemptContext, error) { + return NewAttemptContext( + "session", "kg", []byte{0x01}, + [MessageDigestLength]byte{0x05}, 4, + []group.MemberIndex{1, 2, 3}, + []group.MemberIndex{4}, + ) + }, + }, + { + name: "different included set", + fn: func() (AttemptContext, error) { + return NewAttemptContext( + "session", "kg", []byte{0x01}, + [MessageDigestLength]byte{0x05}, 3, + []group.MemberIndex{1, 2, 3, 5}, + []group.MemberIndex{4}, + ) + }, + }, + { + name: "different excluded set", + fn: func() (AttemptContext, error) { + return NewAttemptContext( + "session", "kg", []byte{0x01}, + [MessageDigestLength]byte{0x05}, 3, + []group.MemberIndex{1, 2, 3}, + nil, + ) + }, + }, + { + name: "different DKG public key", + fn: func() (AttemptContext, error) { + return NewAttemptContext( + "session", "kg", []byte{0x02}, + [MessageDigestLength]byte{0x05}, 3, + []group.MemberIndex{1, 2, 3}, + []group.MemberIndex{4}, + ) + }, + }, + } + + for _, m := range mutators { + t.Run(m.name, func(t *testing.T) { + ctx, err := m.fn() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.Hash() == baseHash { + t.Fatalf( + "%s did not change hash; base=%x mutated=%x", + m.name, baseHash, ctx.Hash(), + ) + } + }) + } +} + +func TestAttemptContextHash_PrefixesAvoidStringConcatCollision(t *testing.T) { + // Without length-prefixed encoding, ("ab", "cd") and ("a", "bcd") would + // produce identical hashes. Verify they do not. + dkgPub := []byte{0x01} + digest := [MessageDigestLength]byte{} + + ctxA, err := NewAttemptContext( + "ab", "cd", dkgPub, digest, 0, + []group.MemberIndex{1}, nil, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + ctxB, err := NewAttemptContext( + "a", "bcd", dkgPub, digest, 0, + []group.MemberIndex{1}, nil, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctxA.Hash() == ctxB.Hash() { + t.Fatalf( + "concatenated session+keyGroup collide: hash=%x", + ctxA.Hash(), + ) + } +} + +func TestAttemptContextHash_IsStableAcrossSafeFieldExtensions(t *testing.T) { + // Lock the wire encoding by asserting a specific hash output for a + // pinned fixture. If a future change to the canonical encoding + // changes this hash, that change is a wire-format break and must be + // caught at code review. + ctx, err := NewAttemptContext( + "session-pinned", + "key-group-pinned", + []byte{0xAA, 0xBB, 0xCC, 0xDD}, + [MessageDigestLength]byte{ + 0x00, 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, + }, + 42, + []group.MemberIndex{1, 2, 3}, + []group.MemberIndex{4, 5}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Recompute the expected hash by independently re-implementing the + // canonical encoding here so the test catches accidental drift in + // either the production encoder or the expected hash literal. + want := referenceHashForFixture(ctx) + got := ctx.Hash() + if got != want { + t.Fatalf( + "pinned fixture hash drifted: got %x want %x", + got, want, + ) + } +} + +func memberSlicesEqual(a, b []group.MemberIndex) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +// referenceHashForFixture implements the canonical encoding inline so +// the pinned-fixture test catches drift in either the production +// implementation or the test literal. +func referenceHashForFixture(ctx AttemptContext) [MessageDigestLength]byte { + h := sha256.New() + writeLP := func(b []byte) { + var l [4]byte + binary.BigEndian.PutUint32(l[:], uint32(len(b))) + h.Write(l[:]) + h.Write(b) + } + writeMS := func(ms []group.MemberIndex) { + var l [4]byte + binary.BigEndian.PutUint32(l[:], uint32(len(ms))) + h.Write(l[:]) + for _, m := range ms { + h.Write([]byte{byte(m)}) + } + } + + writeLP([]byte(ctx.SessionID)) + writeLP([]byte(ctx.KeyGroupID)) + h.Write(ctx.MessageDigest[:]) + var a [4]byte + binary.BigEndian.PutUint32(a[:], ctx.AttemptNumber) + h.Write(a[:]) + writeMS(ctx.IncludedSet) + writeMS(ctx.ExcludedSet) + writeMS(ctx.TransientlyParked) + h.Write(ctx.AttemptSeed[:]) + var out [MessageDigestLength]byte + copy(out[:], h.Sum(nil)) + return out +} diff --git a/pkg/frost/roast/attempt/evidence_recorder.go b/pkg/frost/roast/attempt/evidence_recorder.go new file mode 100644 index 0000000000..b67d23513c --- /dev/null +++ b/pkg/frost/roast/attempt/evidence_recorder.go @@ -0,0 +1,253 @@ +package attempt + +import ( + "sync" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// OverflowQuotaDefault is the default per-sender overflow event quota +// enforced by NewBoundedRecorder. It matches the categoryQuota.Overflow +// value documented in RFC-21 Layer A. +// +// A peer that overflows the inbound message channel more than the +// quota allows in a single attempt is recorded only up to the quota: +// further overflows are silently dropped by the recorder. This bounds +// the per-attempt evidence size to O(|IncludedSet| * quota) regardless +// of how aggressively a peer (or its network link) misbehaves. +const OverflowQuotaDefault uint = 8 + +// RejectQuotaDefault is the default per-sender reject event quota. +// Matches categoryQuota.Reject in RFC-21 Layer A. A reject event is +// recorded each time a peer's payload fails the validation gate +// (shouldAcceptNativeFROSTMessage returning false), regardless of +// the specific reason. +const RejectQuotaDefault uint = 8 + +// ConflictQuotaDefault is the default per-sender conflict event +// quota. Matches categoryQuota.Conflict in RFC-21 Layer A. A +// conflict event is recorded when a peer retransmits a message for +// a sender slot that already holds a byte-different contribution +// (first-write-wins reject). +const ConflictQuotaDefault uint = 4 + +// EvidenceRecorder collects bounded, per-attempt evidence of receive- +// path anomalies that the ROAST coordinator's exclusion policy may +// later consume. +// +// The interface tracks three categories of evidence: +// - Overflow: payload arrived but the inbound channel was full. +// - Reject: payload arrived but failed validation +// (shouldAcceptNativeFROSTMessage returning false). +// - Conflict: a peer's later retransmission disagreed with its +// earlier contribution for the same slot (equivocation +// signal). +// +// Silence -- peers in the IncludedSet that produced no snapshot at +// all -- is derived implicitly by the NextAttempt policy from +// (ctx.IncludedSet - bundleSenders) and does not need a recorder +// method. +// +// Implementations must be safe for concurrent calls from multiple +// goroutines, since the receive-callback closure in pkg/frost/signing +// is driven by network goroutines. +type EvidenceRecorder interface { + // RecordOverflow notes that the inbound message channel was full + // when a payload from the named sender arrived, causing the + // payload to be dropped at the receive callback. The recorder + // applies its own quota; callers do not need to suppress at the + // call site. + RecordOverflow(sender group.MemberIndex) + // RecordReject notes that a payload from the named sender failed + // the validation gate (typically shouldAcceptNativeFROSTMessage + // returning false). The reason string is preserved verbatim in + // the snapshot so the coordinator's exclusion policy can later + // route by reason if needed; the recorder applies its own + // per-sender quota regardless of reason. + RecordReject(sender group.MemberIndex, reason string) + // RecordConflict notes that a peer retransmitted a message for + // a sender slot that already holds a byte-different contribution + // (equivocation signal under the first-write-wins assembly + // policy). + RecordConflict(sender group.MemberIndex) + // Snapshot returns a copy of the recorded evidence so far. The + // returned value does not alias internal state; the recorder may + // continue receiving events after Snapshot is called. + Snapshot() Evidence +} + +// RejectEntry describes a single per-sender reject event recorded +// during an attempt. The reason captures *why* the validation gate +// rejected the payload; the coordinator's exclusion policy treats +// every distinct reason as equally blamable today, but the field +// is kept structured so future policy refinements can differentiate. +type RejectEntry struct { + Reason string + Count uint +} + +// Evidence is the per-attempt snapshot of receive-path anomalies +// captured by an EvidenceRecorder. It is the value the ROAST +// coordinator's NextAttempt policy consumes to derive the next +// attempt's ExcludedSet. +// +// Maps are nil-safe in callers: an absent key means the category +// did not fire for that sender, count zero. +type Evidence struct { + // Overflows maps each sender to the number of overflow events + // observed for that sender during the attempt, saturated at the + // recorder's overflow quota. + Overflows map[group.MemberIndex]uint + // Rejects maps each sender to a per-reason set of reject entries. + // The outer map's key is the sender; the inner slice carries one + // entry per distinct reason, with Count saturated at the + // recorder's reject quota. + Rejects map[group.MemberIndex][]RejectEntry + // Conflicts maps each sender to the number of first-write-wins + // conflict events observed during the attempt, saturated at the + // recorder's conflict quota. + Conflicts map[group.MemberIndex]uint +} + +// NewBoundedRecorder returns an EvidenceRecorder with default +// per-sender quotas across all three categories. The recorder is +// safe for concurrent use. +func NewBoundedRecorder() EvidenceRecorder { + return NewBoundedRecorderWithQuotas( + OverflowQuotaDefault, + RejectQuotaDefault, + ConflictQuotaDefault, + ) +} + +// NewBoundedRecorderWithQuota returns a recorder with a custom +// overflow quota; reject and conflict quotas use their defaults. +// Preserved as the Phase-2 entry point so existing callers do not +// need to update. +func NewBoundedRecorderWithQuota(overflowQuota uint) EvidenceRecorder { + return NewBoundedRecorderWithQuotas( + overflowQuota, + RejectQuotaDefault, + ConflictQuotaDefault, + ) +} + +// NewBoundedRecorderWithQuotas returns a recorder with custom +// per-category quotas. Intended for tests; production callers +// should use NewBoundedRecorder so the per-attempt evidence size +// is uniform across the network. +func NewBoundedRecorderWithQuotas( + overflowQuota, rejectQuota, conflictQuota uint, +) EvidenceRecorder { + return &boundedRecorder{ + overflowQuota: overflowQuota, + rejectQuota: rejectQuota, + conflictQuota: conflictQuota, + overflows: map[group.MemberIndex]uint{}, + rejects: map[group.MemberIndex]map[string]uint{}, + conflicts: map[group.MemberIndex]uint{}, + } +} + +// NoOpRecorder returns a recorder that discards every event and +// reports an empty Evidence on Snapshot. It is the default at +// every receive-loop call site when the ROAST-retry registry is +// not populated, so the receive loops' observable behaviour stays +// identical to pre-Phase-2 until a real recorder is wired. +func NoOpRecorder() EvidenceRecorder { + return noOpRecorder{} +} + +type boundedRecorder struct { + mu sync.Mutex + overflowQuota uint + rejectQuota uint + conflictQuota uint + overflows map[group.MemberIndex]uint + // rejects[sender][reason] = count. The two-level map keeps each + // reason bucket bounded by rejectQuota independently so a peer + // cannot saturate one reason to mask another (RFC-21 Layer A: + // "a peer cannot spam overflow events to drown out reject + // evidence or vice-versa"; the same principle applies within + // reject reasons). + rejects map[group.MemberIndex]map[string]uint + conflicts map[group.MemberIndex]uint +} + +func (r *boundedRecorder) RecordOverflow(sender group.MemberIndex) { + r.mu.Lock() + defer r.mu.Unlock() + if r.overflows[sender] < r.overflowQuota { + r.overflows[sender]++ + } +} + +func (r *boundedRecorder) RecordReject( + sender group.MemberIndex, + reason string, +) { + r.mu.Lock() + defer r.mu.Unlock() + bySender, ok := r.rejects[sender] + if !ok { + bySender = map[string]uint{} + r.rejects[sender] = bySender + } + if bySender[reason] < r.rejectQuota { + bySender[reason]++ + } +} + +func (r *boundedRecorder) RecordConflict(sender group.MemberIndex) { + r.mu.Lock() + defer r.mu.Unlock() + if r.conflicts[sender] < r.conflictQuota { + r.conflicts[sender]++ + } +} + +func (r *boundedRecorder) Snapshot() Evidence { + r.mu.Lock() + defer r.mu.Unlock() + overflows := make(map[group.MemberIndex]uint, len(r.overflows)) + for sender, count := range r.overflows { + overflows[sender] = count + } + rejects := make( + map[group.MemberIndex][]RejectEntry, + len(r.rejects), + ) + for sender, reasons := range r.rejects { + entries := make([]RejectEntry, 0, len(reasons)) + for reason, count := range reasons { + entries = append(entries, RejectEntry{ + Reason: reason, + Count: count, + }) + } + rejects[sender] = entries + } + conflicts := make(map[group.MemberIndex]uint, len(r.conflicts)) + for sender, count := range r.conflicts { + conflicts[sender] = count + } + return Evidence{ + Overflows: overflows, + Rejects: rejects, + Conflicts: conflicts, + } +} + +type noOpRecorder struct{} + +func (noOpRecorder) RecordOverflow(group.MemberIndex) {} +func (noOpRecorder) RecordReject(group.MemberIndex, string) {} +func (noOpRecorder) RecordConflict(group.MemberIndex) {} + +func (noOpRecorder) Snapshot() Evidence { + return Evidence{ + Overflows: map[group.MemberIndex]uint{}, + Rejects: map[group.MemberIndex][]RejectEntry{}, + Conflicts: map[group.MemberIndex]uint{}, + } +} diff --git a/pkg/frost/roast/attempt/evidence_recorder_categories_test.go b/pkg/frost/roast/attempt/evidence_recorder_categories_test.go new file mode 100644 index 0000000000..176d61f152 --- /dev/null +++ b/pkg/frost/roast/attempt/evidence_recorder_categories_test.go @@ -0,0 +1,114 @@ +package attempt + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestBoundedRecorder_RecordReject_AccumulatesByReason(t *testing.T) { + rec := NewBoundedRecorder() + rec.RecordReject(1, "validation_gate_rejected") + rec.RecordReject(1, "validation_gate_rejected") + rec.RecordReject(1, "some_other_reason") + + snap := rec.Snapshot() + entries := snap.Rejects[1] + if len(entries) != 2 { + t.Fatalf("expected 2 reject reasons, got %d", len(entries)) + } + counts := map[string]uint{} + for _, e := range entries { + counts[e.Reason] = e.Count + } + if counts["validation_gate_rejected"] != 2 { + t.Fatalf("validation_gate_rejected count: got %d want 2", counts["validation_gate_rejected"]) + } + if counts["some_other_reason"] != 1 { + t.Fatalf("some_other_reason count: got %d want 1", counts["some_other_reason"]) + } +} + +func TestBoundedRecorder_RecordReject_PerReasonQuota(t *testing.T) { + rec := NewBoundedRecorderWithQuotas(8, 3, 4) + for i := 0; i < 10; i++ { + rec.RecordReject(1, "spam") + } + snap := rec.Snapshot() + got := snap.Rejects[1][0].Count + if got != 3 { + t.Fatalf("reject quota not enforced: got %d, want 3", got) + } +} + +func TestBoundedRecorder_RecordReject_PerReasonQuotasIndependent(t *testing.T) { + // A peer cannot saturate one reason to mask another -- each + // reason has its own quota counter. + rec := NewBoundedRecorderWithQuotas(8, 2, 4) + for i := 0; i < 10; i++ { + rec.RecordReject(1, "reason-A") + } + rec.RecordReject(1, "reason-B") + snap := rec.Snapshot() + counts := map[string]uint{} + for _, e := range snap.Rejects[1] { + counts[e.Reason] = e.Count + } + if counts["reason-A"] != 2 { + t.Fatalf("reason-A saturated at: got %d want 2", counts["reason-A"]) + } + if counts["reason-B"] != 1 { + t.Fatalf("reason-B counted independently: got %d want 1", counts["reason-B"]) + } +} + +func TestBoundedRecorder_RecordConflict_AccumulatesAndSaturates(t *testing.T) { + rec := NewBoundedRecorderWithQuotas(8, 8, 2) + rec.RecordConflict(7) + rec.RecordConflict(7) + rec.RecordConflict(7) + rec.RecordConflict(7) + snap := rec.Snapshot() + if got := snap.Conflicts[7]; got != 2 { + t.Fatalf("conflict count saturated at quota; got %d want 2", got) + } +} + +func TestBoundedRecorder_AllCategoriesPresentInSnapshot(t *testing.T) { + rec := NewBoundedRecorder() + rec.RecordOverflow(1) + rec.RecordReject(2, "validation_gate_rejected") + rec.RecordConflict(3) + snap := rec.Snapshot() + if snap.Overflows[1] == 0 { + t.Fatal("overflow not recorded") + } + if len(snap.Rejects[2]) == 0 { + t.Fatal("reject not recorded") + } + if snap.Conflicts[3] == 0 { + t.Fatal("conflict not recorded") + } +} + +func TestNoOpRecorder_AllCategoriesInert(t *testing.T) { + rec := NoOpRecorder() + for i := 0; i < 100; i++ { + rec.RecordOverflow(group.MemberIndex(i % 5)) + rec.RecordReject(group.MemberIndex(i%5), "spam") + rec.RecordConflict(group.MemberIndex(i % 5)) + } + snap := rec.Snapshot() + if len(snap.Overflows) != 0 || len(snap.Rejects) != 0 || len(snap.Conflicts) != 0 { + t.Fatalf("NoOp recorder must report empty snapshot; got %+v", snap) + } +} + +func TestRejectAndConflictQuotaConstants_MatchRFC(t *testing.T) { + if RejectQuotaDefault != 8 { + t.Fatalf("RFC-21 specifies reject quota = 8; constant is %d", RejectQuotaDefault) + } + if ConflictQuotaDefault != 4 { + t.Fatalf("RFC-21 specifies conflict quota = 4; constant is %d", ConflictQuotaDefault) + } +} diff --git a/pkg/frost/roast/attempt/evidence_recorder_test.go b/pkg/frost/roast/attempt/evidence_recorder_test.go new file mode 100644 index 0000000000..c36ba6abc3 --- /dev/null +++ b/pkg/frost/roast/attempt/evidence_recorder_test.go @@ -0,0 +1,141 @@ +package attempt + +import ( + "sync" + "testing" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestNoOpRecorder_IsObservablyInert(t *testing.T) { + rec := NoOpRecorder() + for i := 0; i < 1000; i++ { + rec.RecordOverflow(group.MemberIndex(i%5 + 1)) + } + snap := rec.Snapshot() + if len(snap.Overflows) != 0 { + t.Fatalf( + "NoOp recorder must report zero overflows; got %d entries", + len(snap.Overflows), + ) + } +} + +func TestBoundedRecorder_CountsOverflowsBySender(t *testing.T) { + rec := NewBoundedRecorder() + rec.RecordOverflow(1) + rec.RecordOverflow(2) + rec.RecordOverflow(1) + + snap := rec.Snapshot() + if got := snap.Overflows[1]; got != 2 { + t.Fatalf("sender 1 overflow count: got %d want 2", got) + } + if got := snap.Overflows[2]; got != 1 { + t.Fatalf("sender 2 overflow count: got %d want 1", got) + } + if _, ok := snap.Overflows[3]; ok { + t.Fatal("sender 3 should have no entry") + } +} + +func TestBoundedRecorder_SaturatesAtQuota(t *testing.T) { + const quota uint = 4 + rec := NewBoundedRecorderWithQuota(quota) + + for i := uint(0); i < quota+10; i++ { + rec.RecordOverflow(1) + } + snap := rec.Snapshot() + if got := snap.Overflows[1]; got != quota { + t.Fatalf( + "overflow count must saturate at quota %d; got %d", + quota, got, + ) + } +} + +func TestBoundedRecorder_DefaultQuotaIs8(t *testing.T) { + rec := NewBoundedRecorder() + for i := 0; i < 100; i++ { + rec.RecordOverflow(1) + } + if got := rec.Snapshot().Overflows[1]; got != OverflowQuotaDefault { + t.Fatalf( + "default quota mismatch; got %d want %d", + got, OverflowQuotaDefault, + ) + } + if OverflowQuotaDefault != 8 { + t.Fatalf( + "RFC-21 Layer A specifies overflow quota = 8; constant is %d", + OverflowQuotaDefault, + ) + } +} + +func TestBoundedRecorder_SnapshotIsDeepCopy(t *testing.T) { + rec := NewBoundedRecorder() + rec.RecordOverflow(1) + rec.RecordOverflow(1) + + snap := rec.Snapshot() + snap.Overflows[1] = 999 + snap.Overflows[42] = 7 + + freshSnap := rec.Snapshot() + if got := freshSnap.Overflows[1]; got != 2 { + t.Fatalf( + "snapshot mutation leaked into recorder state: got %d want 2", + got, + ) + } + if _, ok := freshSnap.Overflows[42]; ok { + t.Fatal("snapshot mutation leaked a new key into recorder state") + } +} + +func TestBoundedRecorder_ConcurrentRecordersAreRaceSafe(t *testing.T) { + const ( + recordersPerSender = 8 + sendersN = 16 + recordsPerRecorder = 200 + ) + rec := NewBoundedRecorderWithQuota(uint(recordersPerSender * recordsPerRecorder * 10)) + + var wg sync.WaitGroup + for senderIdx := 1; senderIdx <= sendersN; senderIdx++ { + sender := group.MemberIndex(senderIdx) + for w := 0; w < recordersPerSender; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for n := 0; n < recordsPerRecorder; n++ { + rec.RecordOverflow(sender) + } + }() + } + } + wg.Wait() + + snap := rec.Snapshot() + for senderIdx := 1; senderIdx <= sendersN; senderIdx++ { + want := uint(recordersPerSender * recordsPerRecorder) + if got := snap.Overflows[group.MemberIndex(senderIdx)]; got != want { + t.Fatalf( + "sender %d concurrent count: got %d want %d", + senderIdx, got, want, + ) + } + } +} + +func TestNoOpRecorder_DistinctInstancesShareSemantics(t *testing.T) { + a := NoOpRecorder() + b := NoOpRecorder() + a.RecordOverflow(1) + b.RecordOverflow(2) + if len(a.Snapshot().Overflows) != 0 || len(b.Snapshot().Overflows) != 0 { + t.Fatal("NoOp instances must not retain state") + } +} diff --git a/pkg/frost/roast/bundle_aggregation_test.go b/pkg/frost/roast/bundle_aggregation_test.go new file mode 100644 index 0000000000..412c63db24 --- /dev/null +++ b/pkg/frost/roast/bundle_aggregation_test.go @@ -0,0 +1,564 @@ +package roast + +import ( + "bytes" + "errors" + "sync" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// pickNonCoordinatorMember returns the first member of `set` that is +// not equal to `elected`. Fatals if no such member exists. Used by +// receiver-side tests that need a member distinct from the +// aggregator. +func pickNonCoordinatorMember( + t *testing.T, + set []group.MemberIndex, + elected group.MemberIndex, +) group.MemberIndex { + t.Helper() + for _, m := range set { + if m != elected { + return m + } + } + t.Fatalf("no non-coordinator member available; set=%v elected=%d", set, elected) + return 0 +} + +// signSnapshotForTest mints a fakeSigner signature on a snapshot and +// stores it on the snapshot's OperatorSignature field. Returns the +// snapshot for chaining. +func signSnapshotForTest( + t *testing.T, + snap *LocalEvidenceSnapshot, +) *LocalEvidenceSnapshot { + t.Helper() + signer := &fakeSigner{id: snap.SenderID()} + payload, err := CanonicalSnapshotBytes(snap) + if err != nil { + t.Fatalf("canonical: %v", err) + } + sig, err := signer.Sign(payload) + if err != nil { + t.Fatalf("sign: %v", err) + } + snap.OperatorSignature = sig + return snap +} + +// newSignedCoordinatorForMember returns an inMemoryCoordinator wired +// for the named member to act as self -- meaning AggregateBundle is +// only callable when that member is the elected coordinator for the +// attempt under test. +func newSignedCoordinatorForMember( + self group.MemberIndex, +) *inMemoryCoordinator { + return NewInMemoryCoordinatorWithSigning( + self, + &fakeSigner{id: self}, + fakeVerifier{}, + ).(*inMemoryCoordinator) +} + +func TestRecordEvidence_RejectsNilSnapshot(t *testing.T) { + c := newSignedCoordinatorForMember(0) + handle, err := c.BeginAttempt(newTestContext(t)) + if err != nil { + t.Fatalf("begin: %v", err) + } + if err := c.RecordEvidence(handle, nil); err == nil { + t.Fatal("expected nil snapshot error") + } +} + +func TestRecordEvidence_RejectsUnknownHandle(t *testing.T) { + c := newSignedCoordinatorForMember(0) + snap := signSnapshotForTest(t, NewLocalEvidenceSnapshot(1, pinnedContextHash, attempt.Evidence{})) + bogus := AttemptHandle{id: 999} + err := c.RecordEvidence(bogus, snap) + if !errors.Is(err, ErrUnknownAttempt) { + t.Fatalf("expected ErrUnknownAttempt, got %v", err) + } +} + +func TestRecordEvidence_RejectsContextHashMismatch(t *testing.T) { + c := newSignedCoordinatorForMember(0) + handle, err := c.BeginAttempt(newTestContext(t)) + if err != nil { + t.Fatalf("begin: %v", err) + } + // Build a snapshot bound to a *different* context hash. + wrongHash := [attempt.MessageDigestLength]byte{0xff} + snap := signSnapshotForTest(t, NewLocalEvidenceSnapshot(1, wrongHash, attempt.Evidence{})) + if err := c.RecordEvidence(handle, snap); !errors.Is(err, ErrAttemptContextMismatch) { + t.Fatalf("expected ErrAttemptContextMismatch, got %v", err) + } +} + +func TestRecordEvidence_RejectsBadSignature(t *testing.T) { + c := newSignedCoordinatorForMember(0) + ctx := newTestContext(t) + handle, err := c.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + snap := NewLocalEvidenceSnapshot(1, ctx.Hash(), attempt.Evidence{}) + snap.OperatorSignature = []byte{0xff, 0xee} + err = c.RecordEvidence(handle, snap) + if !errors.Is(err, ErrSignatureInvalid) { + t.Fatalf("expected ErrSignatureInvalid, got %v", err) + } +} + +func TestRecordEvidence_AcceptsValidSnapshotAndIsIdempotent(t *testing.T) { + c := newSignedCoordinatorForMember(0) + ctx := newTestContext(t) + handle, err := c.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + snap := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(1, ctx.Hash(), attempt.Evidence{}), + ) + if err := c.RecordEvidence(handle, snap); err != nil { + t.Fatalf("first record: %v", err) + } + // Identical re-submission must be idempotent. + if err := c.RecordEvidence(handle, snap); err != nil { + t.Fatalf("idempotent re-record: %v", err) + } +} + +func TestRecordEvidence_RejectsConflict(t *testing.T) { + c := newSignedCoordinatorForMember(0) + ctx := newTestContext(t) + handle, err := c.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + first := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(1, ctx.Hash(), attempt.Evidence{}), + ) + if err := c.RecordEvidence(handle, first); err != nil { + t.Fatalf("first record: %v", err) + } + // Same sender, different evidence -> conflict. + conflicting := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(1, ctx.Hash(), attempt.Evidence{ + Overflows: map[group.MemberIndex]uint{5: 3}, + }), + ) + if err := c.RecordEvidence(handle, conflicting); !errors.Is(err, ErrSnapshotConflict) { + t.Fatalf("expected ErrSnapshotConflict, got %v", err) + } +} + +func TestRecordEvidence_TracksSelfSubmission(t *testing.T) { + const self group.MemberIndex = 3 + c := newSignedCoordinatorForMember(self) + ctx := newTestContext(t) + handle, err := c.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + selfSnap := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(self, ctx.Hash(), attempt.Evidence{}), + ) + if err := c.RecordEvidence(handle, selfSnap); err != nil { + t.Fatalf("record self: %v", err) + } + record := c.attempts[handle.id] + if record.selfSubmission == nil { + t.Fatal("expected selfSubmission to be set") + } + if record.selfSubmission.SenderID() != self { + t.Fatalf("self submission member mismatch: got %d", record.selfSubmission.SenderID()) + } +} + +func TestAggregateBundle_RejectsNonAggregator(t *testing.T) { + // Two coordinator instances, both begin the same attempt. Only + // the elected one can aggregate. We force the election by + // building a context where SelectCoordinator will pick member 1. + c := NewInMemoryCoordinatorWithSigning(99, &fakeSigner{id: 99}, fakeVerifier{}).(*inMemoryCoordinator) + handle, err := c.BeginAttempt(newTestContext(t)) + if err != nil { + t.Fatalf("begin: %v", err) + } + // member 99 is not in the IncludedSet, so it cannot be the + // elected coordinator. + _, err = c.AggregateBundle(handle) + if !errors.Is(err, ErrNotAggregator) { + t.Fatalf("expected ErrNotAggregator, got %v", err) + } +} + +func TestAggregateBundle_BuildsSignedBundle(t *testing.T) { + // Pick the elected coordinator: run BeginAttempt once with a + // throwaway coordinator instance to discover the elected member, + // then build a real coordinator bound to that self. + scratch := NewInMemoryCoordinator().(*inMemoryCoordinator) + ctx := newTestContext(t) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + c := newSignedCoordinatorForMember(elected) + handle, err := c.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + // Record snapshots from every included member. + for _, m := range ctx.IncludedSet { + snap := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{}), + ) + if err := c.RecordEvidence(handle, snap); err != nil { + t.Fatalf("record %d: %v", m, err) + } + } + bundle, err := c.AggregateBundle(handle) + if err != nil { + t.Fatalf("aggregate: %v", err) + } + if len(bundle.Bundle) != len(ctx.IncludedSet) { + t.Fatalf( + "bundle size: got %d want %d", + len(bundle.Bundle), len(ctx.IncludedSet), + ) + } + for i := 1; i < len(bundle.Bundle); i++ { + if bundle.Bundle[i].SenderIDValue <= bundle.Bundle[i-1].SenderIDValue { + t.Fatalf("bundle not sorted ascending at %d", i) + } + } + if bundle.CoordinatorID() != elected { + t.Fatalf("bundle coordinator id %d != elected %d", bundle.CoordinatorID(), elected) + } + if len(bundle.CoordinatorSignature) == 0 { + t.Fatal("expected coordinator signature to be populated") + } + state, _ := c.State(handle) + if state != AttemptStateTransitioned { + t.Fatalf("expected state Transitioned, got %v", state) + } +} + +func TestAggregateBundle_ProducesDeterministicBundleAcrossOrderings(t *testing.T) { + // Two coordinators aggregate the same evidence in different + // arrival orders. The resulting bundles must be byte-identical + // after JSON marshal. + scratch := NewInMemoryCoordinator().(*inMemoryCoordinator) + ctx := newTestContext(t) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + make := func( + t *testing.T, + recordOrder []group.MemberIndex, + ) []byte { + t.Helper() + c := newSignedCoordinatorForMember(elected) + handle, err := c.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + for _, m := range recordOrder { + snap := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{}), + ) + if err := c.RecordEvidence(handle, snap); err != nil { + t.Fatalf("record %d: %v", m, err) + } + } + bundle, err := c.AggregateBundle(handle) + if err != nil { + t.Fatalf("aggregate: %v", err) + } + data, err := bundle.Marshal() + if err != nil { + t.Fatalf("marshal: %v", err) + } + return data + } + ordering1 := []group.MemberIndex{1, 2, 3, 4, 5} + ordering2 := []group.MemberIndex{5, 3, 1, 4, 2} + a := make(t, ordering1) + b := make(t, ordering2) + if !bytes.Equal(a, b) { + t.Fatalf( + "identical evidence in different arrival order produced "+ + "different bundles:\n a=%s\n b=%s", + string(a), string(b), + ) + } +} + +func TestVerifyBundle_AcceptsValidBundle(t *testing.T) { + scratch := NewInMemoryCoordinator().(*inMemoryCoordinator) + ctx := newTestContext(t) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + aggregator := newSignedCoordinatorForMember(elected) + handle, err := aggregator.BeginAttempt(ctx) + if err != nil { + t.Fatalf("aggregator begin: %v", err) + } + for _, m := range ctx.IncludedSet { + snap := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{}), + ) + if err := aggregator.RecordEvidence(handle, snap); err != nil { + t.Fatalf("record %d: %v", m, err) + } + } + bundle, err := aggregator.AggregateBundle(handle) + if err != nil { + t.Fatalf("aggregate: %v", err) + } + + // Receiver: a different coordinator instance bound to a + // non-coordinator member that has not submitted its own snapshot. + // The receiver must accept the bundle. + receiverID := pickNonCoordinatorMember(t, ctx.IncludedSet, elected) + receiver := NewInMemoryCoordinatorWithSigning( + receiverID, + &fakeSigner{id: receiverID}, + fakeVerifier{}, + ).(*inMemoryCoordinator) + rh, err := receiver.BeginAttempt(ctx) + if err != nil { + t.Fatalf("receiver begin: %v", err) + } + if err := receiver.VerifyBundle(rh, bundle); err != nil { + t.Fatalf("expected verify success, got %v", err) + } +} + +func TestVerifyBundle_DetectsCensorship(t *testing.T) { + scratch := NewInMemoryCoordinator().(*inMemoryCoordinator) + ctx := newTestContext(t) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + aggregator := newSignedCoordinatorForMember(elected) + handle, err := aggregator.BeginAttempt(ctx) + if err != nil { + t.Fatalf("agg begin: %v", err) + } + // Record snapshots from every member EXCEPT receiverID. + receiverID := pickNonCoordinatorMember(t, ctx.IncludedSet, elected) + for _, m := range ctx.IncludedSet { + if m == receiverID { + continue + } + snap := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{}), + ) + if err := aggregator.RecordEvidence(handle, snap); err != nil { + t.Fatalf("record: %v", err) + } + } + bundle, err := aggregator.AggregateBundle(handle) + if err != nil { + t.Fatalf("aggregate: %v", err) + } + + // Receiver: bound to receiverID, has submitted its own snapshot, + // but the coordinator chose to censor it. + receiver := NewInMemoryCoordinatorWithSigning( + receiverID, + &fakeSigner{id: receiverID}, + fakeVerifier{}, + ).(*inMemoryCoordinator) + rh, err := receiver.BeginAttempt(ctx) + if err != nil { + t.Fatalf("receiver begin: %v", err) + } + selfSnap := signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(receiverID, ctx.Hash(), attempt.Evidence{}), + ) + if err := receiver.RecordEvidence(rh, selfSnap); err != nil { + t.Fatalf("receiver record self: %v", err) + } + err = receiver.VerifyBundle(rh, bundle) + if !errors.Is(err, ErrCensorshipDetected) { + t.Fatalf("expected ErrCensorshipDetected, got %v", err) + } +} + +func TestVerifyBundle_DetectsCoordinatorSignatureForgery(t *testing.T) { + scratch := NewInMemoryCoordinator().(*inMemoryCoordinator) + ctx := newTestContext(t) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + aggregator := newSignedCoordinatorForMember(elected) + handle, _ := aggregator.BeginAttempt(ctx) + for _, m := range ctx.IncludedSet { + _ = aggregator.RecordEvidence(handle, signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{}), + )) + } + bundle, _ := aggregator.AggregateBundle(handle) + // Tamper: re-sign the bundle as a different (non-elected) member. + const wrongSigner group.MemberIndex = 99 + bundle.CoordinatorIDValue = uint32(wrongSigner) + payload, _ := CanonicalBundleBytes(bundle) + forged, _ := (&fakeSigner{id: wrongSigner}).Sign(payload) + bundle.CoordinatorSignature = forged + + receiver := NewInMemoryCoordinatorWithSigning( + 7, + &fakeSigner{id: 7}, + fakeVerifier{}, + ).(*inMemoryCoordinator) + rh, _ := receiver.BeginAttempt(ctx) + err := receiver.VerifyBundle(rh, bundle) + if err == nil { + t.Fatal("expected verification failure") + } +} + +func TestVerifyBundle_DetectsSnapshotSignatureForgery(t *testing.T) { + scratch := NewInMemoryCoordinator().(*inMemoryCoordinator) + ctx := newTestContext(t) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + aggregator := newSignedCoordinatorForMember(elected) + handle, _ := aggregator.BeginAttempt(ctx) + for _, m := range ctx.IncludedSet { + _ = aggregator.RecordEvidence(handle, signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{}), + )) + } + bundle, _ := aggregator.AggregateBundle(handle) + + // Tamper: replace one snapshot's signature with garbage. The + // bundle's coordinator signature still validates (since the + // canonical bundle bytes include the snapshot signature, an + // integrated bundle would have detected the change at the + // coordinator-signature layer). For this test we re-sign the + // bundle with the new garbage signature so the bundle-level + // signature appears valid but the snapshot signature does not. + bundle.Bundle[0].OperatorSignature = []byte{0xde, 0xad} + payload, _ := CanonicalBundleBytes(bundle) + resign, _ := (&fakeSigner{id: elected}).Sign(payload) + bundle.CoordinatorSignature = resign + + receiver := NewInMemoryCoordinatorWithSigning( + 7, + &fakeSigner{id: 7}, + fakeVerifier{}, + ).(*inMemoryCoordinator) + rh, _ := receiver.BeginAttempt(ctx) + err := receiver.VerifyBundle(rh, bundle) + if !errors.Is(err, ErrSignatureInvalid) { + t.Fatalf("expected ErrSignatureInvalid, got %v", err) + } +} + +func TestVerifyBundle_RejectsAttemptContextMismatch(t *testing.T) { + scratch := NewInMemoryCoordinator().(*inMemoryCoordinator) + ctx := newTestContext(t) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + aggregator := newSignedCoordinatorForMember(elected) + handle, _ := aggregator.BeginAttempt(ctx) + for _, m := range ctx.IncludedSet { + _ = aggregator.RecordEvidence(handle, signSnapshotForTest( + t, + NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{}), + )) + } + bundle, _ := aggregator.AggregateBundle(handle) + + receiver := NewInMemoryCoordinatorWithSigning( + 7, + &fakeSigner{id: 7}, + fakeVerifier{}, + ).(*inMemoryCoordinator) + + // Receiver begins a different attempt context. + wrongCtx, _ := attempt.NewAttemptContext( + "different-session", + "key-group-test", + []byte{0xab, 0xcd, 0xef}, + [attempt.MessageDigestLength]byte{0x42}, + 0, + []group.MemberIndex{1, 2, 3, 4, 5}, + nil, + ) + rh, _ := receiver.BeginAttempt(wrongCtx) + err := receiver.VerifyBundle(rh, bundle) + if !errors.Is(err, ErrAttemptContextMismatch) { + t.Fatalf("expected ErrAttemptContextMismatch, got %v", err) + } +} + +func TestVerifyBundle_RejectsNilMessage(t *testing.T) { + c := newSignedCoordinatorForMember(7) + handle, _ := c.BeginAttempt(newTestContext(t)) + if err := c.VerifyBundle(handle, nil); err == nil { + t.Fatal("expected error for nil message") + } +} + +func TestVerifyBundle_RejectsUnknownAttempt(t *testing.T) { + c := newSignedCoordinatorForMember(7) + bundle := buildValidTransitionMessage() + bogus := AttemptHandle{id: 999} + if err := c.VerifyBundle(bogus, bundle); !errors.Is(err, ErrUnknownAttempt) { + t.Fatalf("expected ErrUnknownAttempt, got %v", err) + } +} + +func TestCoordinator_ConcurrentRecordAndVerifyAreRaceSafe(t *testing.T) { + scratch := NewInMemoryCoordinator().(*inMemoryCoordinator) + ctx := newTestContext(t) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + aggregator := newSignedCoordinatorForMember(elected) + handle, _ := aggregator.BeginAttempt(ctx) + var wg sync.WaitGroup + for _, m := range ctx.IncludedSet { + wg.Add(1) + mLocal := m + go func() { + defer wg.Done() + snap := signSnapshotForTest(t, NewLocalEvidenceSnapshot(mLocal, ctx.Hash(), attempt.Evidence{})) + if err := aggregator.RecordEvidence(handle, snap); err != nil { + t.Errorf("concurrent record %d: %v", mLocal, err) + } + }() + } + wg.Wait() + bundle, err := aggregator.AggregateBundle(handle) + if err != nil { + t.Fatalf("aggregate after concurrent records: %v", err) + } + if len(bundle.Bundle) != len(ctx.IncludedSet) { + t.Fatalf( + "bundle size after concurrent records: got %d want %d", + len(bundle.Bundle), len(ctx.IncludedSet), + ) + } +} diff --git a/pkg/frost/roast/coordinator.go b/pkg/frost/roast/coordinator.go new file mode 100644 index 0000000000..574131cf66 --- /dev/null +++ b/pkg/frost/roast/coordinator.go @@ -0,0 +1,46 @@ +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. +// +// The RNG is intentionally deterministic and non-cryptographic. Callers must +// derive attemptSeed from group-agreed, non-grindable session inputs; if an +// adversary can choose or repeatedly grind those inputs, they can bias the +// coordinator selection by searching for a favorable shuffle. +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_state.go b/pkg/frost/roast/coordinator_state.go new file mode 100644 index 0000000000..afbd32792a --- /dev/null +++ b/pkg/frost/roast/coordinator_state.go @@ -0,0 +1,481 @@ +package roast + +import ( + "bytes" + "errors" + "fmt" + "sort" + "sync" + "sync/atomic" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// AttemptState is the phase an attempt is in within the Coordinator +// state machine. The lifecycle is monotonic: +// +// AttemptStatePending -> AttemptStateCollecting -> AttemptStateAggregating +// -> {AttemptStateSucceeded, AttemptStateTransitioned} +// +// AttemptStateSucceeded means the attempt produced a final signature. +// AttemptStateTransitioned means the attempt timed out or hit an +// unrecoverable reject and the coordinator emitted a +// TransitionMessage that drives the next attempt's context. Phase 3.1 +// (this file) introduces the state surface only; later phases drive +// the transitions. +type AttemptState uint8 + +const ( + // AttemptStatePending is the zero value -- not a real state, used + // only as the default-initialised "unknown" sentinel returned with + // ErrUnknownAttempt. + AttemptStatePending AttemptState = iota + // AttemptStateCollecting -- the attempt has been started, the + // included set is fixed, and the coordinator is accepting signed + // evidence snapshots from peers. + AttemptStateCollecting + // AttemptStateAggregating -- the coordinator has stopped + // accepting evidence and is building the TransitionMessage + // bundle. + AttemptStateAggregating + // AttemptStateSucceeded -- the attempt produced a final + // signature; no transition message is needed. + AttemptStateSucceeded + // AttemptStateTransitioned -- the attempt timed out or failed + // and the coordinator has emitted a TransitionMessage; the next + // attempt's context can now be computed by NextAttempt. + AttemptStateTransitioned +) + +func (s AttemptState) String() string { + switch s { + case AttemptStatePending: + return "pending" + case AttemptStateCollecting: + return "collecting" + case AttemptStateAggregating: + return "aggregating" + case AttemptStateSucceeded: + return "succeeded" + case AttemptStateTransitioned: + return "transitioned" + default: + return fmt.Sprintf("unknown(%d)", uint8(s)) + } +} + +// AttemptHandle is the opaque per-attempt identity returned by +// Coordinator.BeginAttempt. Handles are not interchangeable across +// coordinator instances: a handle minted by coordinator A cannot be +// passed to coordinator B. Callers must not mutate handles directly. +type AttemptHandle struct { + id uint64 + contextHash [attempt.MessageDigestLength]byte +} + +// ContextHash returns the canonical AttemptContext.Hash() value bound +// to this handle. Useful for cross-checking a handle against a +// context after the fact. +func (h AttemptHandle) ContextHash() [attempt.MessageDigestLength]byte { + return h.contextHash +} + +// Coordinator is the ROAST coordinator state machine introduced by +// RFC-21 Phase 3. It owns per-attempt state, the deterministic +// participant selection (via the existing SelectCoordinator helper), +// signed-evidence aggregation, transition-message construction, and +// -- in Phase 3.4 -- the NextAttempt policy. +// +// Phase 3.1 introduced BeginAttempt, State, and SelectedCoordinator. +// Phase 3.3 (this commit) adds RecordEvidence, AggregateBundle, and +// VerifyBundle. +// Phase 3.4 will add NextAttempt. +// +// Implementations must be safe for concurrent calls from multiple +// goroutines; production keep-core code paths are network-driven. +type Coordinator interface { + // BeginAttempt initialises tracking for a new attempt with the + // given context. It selects the attempt's coordinator + // deterministically from ctx.IncludedSet via SelectCoordinator + // (with the legacy int64 seed produced by foldAttemptSeed) and + // stores the result on the returned handle. + BeginAttempt(ctx attempt.AttemptContext) (AttemptHandle, error) + // State returns the current AttemptState for the given handle. + // Returns ErrUnknownAttempt if the handle was not produced by + // this Coordinator instance. + State(handle AttemptHandle) (AttemptState, error) + // SelectedCoordinator returns the member elected as coordinator + // for the attempt identified by the handle. Returns + // ErrUnknownAttempt if the handle is not tracked. + SelectedCoordinator(handle AttemptHandle) (group.MemberIndex, error) + // RecordEvidence stores a peer's signed LocalEvidenceSnapshot + // against the named attempt. The snapshot is validated for + // structural correctness, its OperatorSignature is verified + // against the configured SignatureVerifier, and its + // AttemptContextHash is checked to match the handle's bound + // context. First-write-wins / equal-or-reject semantics apply: + // a peer that re-submits the same byte-identical snapshot is + // idempotent; a peer that mutates its snapshot returns an error + // without overwriting the originally accepted one. + RecordEvidence(handle AttemptHandle, snapshot *LocalEvidenceSnapshot) error + // AggregateBundle is called by the elected coordinator's node + // to produce a TransitionMessage from the accumulated evidence + // snapshots. The bundle is sorted ascending by SenderID, signed + // with the coordinator's Signer, and the attempt state is + // transitioned to AttemptStateAggregating then + // AttemptStateTransitioned. + // + // Returns ErrNotAggregator if the caller is not the elected + // coordinator for the attempt (the Coordinator's selfMember + // must equal SelectedCoordinator(handle)). + AggregateBundle(handle AttemptHandle) (*TransitionMessage, error) + // VerifyBundle is called by every receiver of a + // TransitionMessage. It validates the structural invariants of + // the bundle, verifies the coordinator-level signature against + // the attempt's elected coordinator, verifies each contained + // snapshot's operator signature, and -- if the receiver has + // already submitted its own snapshot via RecordEvidence with + // the local Signer applied -- verifies that the receiver's own + // snapshot is present and byte-identical in the bundle + // (censorship detection). + // + // Returns ErrCensorshipDetected when the receiver's own + // submitted snapshot is missing or mutated. Returns + // ErrSignatureInvalid when any signature fails verification. + VerifyBundle(handle AttemptHandle, msg *TransitionMessage) error + // NextAttempt computes the deterministic next AttemptContext + // from a verified TransitionMessage. Callers MUST call + // VerifyBundle before NextAttempt; NextAttempt does not + // re-verify signatures. + // + // threshold is the FROST signing threshold t for the key group; + // it is constant across attempts within a session. A threshold + // of zero disables the infeasibility check (test seam). + // + // dkgGroupPublicKey is the DKG-validated group public key from + // the FFI signer material (RFC-21 Decision 2). It is passed + // here so two honest signers derive the same AttemptSeed for + // the next attempt. + // + // Returns ErrAttemptInfeasible when the next IncludedSet would + // drop below threshold. + NextAttempt( + handle AttemptHandle, + bundle *TransitionMessage, + threshold uint, + dkgGroupPublicKey []byte, + ) (attempt.AttemptContext, error) +} + +// ErrNotAggregator is returned by AggregateBundle when the caller +// is not the elected coordinator for the named attempt. +var ErrNotAggregator = errors.New( + "coordinator: caller is not the elected coordinator for this attempt", +) + +// ErrAttemptStateInvalid is returned when an operation is requested +// against an attempt in a state that does not permit it (e.g. +// AggregateBundle on an attempt already transitioned, or +// RecordEvidence on an attempt past Collecting). +var ErrAttemptStateInvalid = errors.New("coordinator: attempt state does not permit operation") + +// ErrAttemptContextMismatch is returned when a snapshot's +// AttemptContextHash does not match the handle's bound context. +var ErrAttemptContextMismatch = errors.New("coordinator: snapshot attempt context hash does not match attempt") + +// ErrSnapshotConflict is returned by RecordEvidence when a peer +// re-submits a snapshot whose canonical bytes differ from the +// previously-accepted snapshot for that peer in this attempt. The +// originally accepted snapshot is retained; the new submission is +// rejected (first-write-wins). +var ErrSnapshotConflict = errors.New("coordinator: snapshot conflicts with previously recorded one (first-write-wins)") + +// ErrUnknownAttempt indicates an AttemptHandle does not correspond to +// any attempt tracked by this Coordinator. Either the handle was +// minted by a different coordinator instance, or the attempt has +// been pruned. +var ErrUnknownAttempt = errors.New("coordinator: unknown attempt handle") + +// NewInMemoryCoordinator returns a Coordinator that tracks attempts +// in-process with no operator-key signing wired in (NoOpSigner + +// NoOpSignatureVerifier). Suitable for tests that exercise only the +// structural state-machine surface; bundle verification will accept +// any signature. +// +// Production Phase-4 callers should use +// NewInMemoryCoordinatorWithSigning to inject the node's real +// operator-key signer and the network's member-key-resolving +// verifier. +func NewInMemoryCoordinator() Coordinator { + return NewInMemoryCoordinatorWithSigning( + 0, + NoOpSigner(), + NoOpSignatureVerifier(), + ) +} + +// NewInMemoryCoordinatorWithSigning returns an in-memory Coordinator +// bound to the node's own member index, the node's operator-key +// Signer, and a SignatureVerifier capable of resolving every member's +// operator key. selfMember = 0 disables the censorship-detection +// check in VerifyBundle (Phase 3.3 default for unit tests; Phase 4 +// always supplies a non-zero value). +func NewInMemoryCoordinatorWithSigning( + selfMember group.MemberIndex, + signer Signer, + verifier SignatureVerifier, +) Coordinator { + return &inMemoryCoordinator{ + attempts: map[uint64]*attemptRecord{}, + selfMember: selfMember, + signer: signer, + verifier: verifier, + } +} + +type attemptRecord struct { + handle AttemptHandle + context attempt.AttemptContext + coordinator group.MemberIndex + state AttemptState + snapshots map[group.MemberIndex]*LocalEvidenceSnapshot + selfSubmission *LocalEvidenceSnapshot +} + +type inMemoryCoordinator struct { + mu sync.Mutex + nextID atomic.Uint64 + attempts map[uint64]*attemptRecord + selfMember group.MemberIndex + signer Signer + verifier SignatureVerifier +} + +func (c *inMemoryCoordinator) BeginAttempt( + ctx attempt.AttemptContext, +) (AttemptHandle, error) { + if len(ctx.IncludedSet) == 0 { + return AttemptHandle{}, fmt.Errorf( + "coordinator: cannot begin attempt with empty included set", + ) + } + coord, err := SelectCoordinator( + ctx.IncludedSet, + foldAttemptSeed(ctx.AttemptSeed), + uint(ctx.AttemptNumber), + ) + if err != nil { + return AttemptHandle{}, fmt.Errorf( + "coordinator: selection failed: %w", + err, + ) + } + handle := AttemptHandle{ + id: c.nextID.Add(1), + contextHash: ctx.Hash(), + } + record := &attemptRecord{ + handle: handle, + context: ctx, + coordinator: coord, + state: AttemptStateCollecting, + snapshots: map[group.MemberIndex]*LocalEvidenceSnapshot{}, + } + c.mu.Lock() + defer c.mu.Unlock() + c.attempts[handle.id] = record + return handle, nil +} + +func (c *inMemoryCoordinator) State( + handle AttemptHandle, +) (AttemptState, error) { + c.mu.Lock() + defer c.mu.Unlock() + record, ok := c.attempts[handle.id] + if !ok { + return AttemptStatePending, ErrUnknownAttempt + } + return record.state, nil +} + +func (c *inMemoryCoordinator) SelectedCoordinator( + handle AttemptHandle, +) (group.MemberIndex, error) { + c.mu.Lock() + defer c.mu.Unlock() + record, ok := c.attempts[handle.id] + if !ok { + return 0, ErrUnknownAttempt + } + return record.coordinator, nil +} + +func (c *inMemoryCoordinator) RecordEvidence( + handle AttemptHandle, + snapshot *LocalEvidenceSnapshot, +) error { + if snapshot == nil { + return errors.New("coordinator: snapshot is nil") + } + if err := snapshot.Validate(); err != nil { + return fmt.Errorf("coordinator: snapshot invalid: %w", err) + } + if err := verifySnapshotSignature(c.verifier, snapshot); err != nil { + return fmt.Errorf("coordinator: %w", err) + } + + c.mu.Lock() + defer c.mu.Unlock() + record, ok := c.attempts[handle.id] + if !ok { + return ErrUnknownAttempt + } + if record.state != AttemptStateCollecting { + return fmt.Errorf( + "%w: state is %v, want %v", + ErrAttemptStateInvalid, + record.state, + AttemptStateCollecting, + ) + } + if !bytes.Equal( + snapshot.AttemptContextHash, + record.handle.contextHash[:], + ) { + return ErrAttemptContextMismatch + } + + if existing, present := record.snapshots[snapshot.SenderID()]; present { + existingBytes, err := CanonicalSnapshotBytes(existing) + if err != nil { + return fmt.Errorf("coordinator: canonical existing: %w", err) + } + newBytes, err := CanonicalSnapshotBytes(snapshot) + if err != nil { + return fmt.Errorf("coordinator: canonical new: %w", err) + } + if !bytes.Equal(existingBytes, newBytes) || + !bytes.Equal(existing.OperatorSignature, snapshot.OperatorSignature) { + return ErrSnapshotConflict + } + // Identical re-submission: idempotent no-op. + return nil + } + record.snapshots[snapshot.SenderID()] = snapshot + if c.selfMember != 0 && snapshot.SenderID() == c.selfMember { + record.selfSubmission = snapshot + } + return nil +} + +func (c *inMemoryCoordinator) AggregateBundle( + handle AttemptHandle, +) (*TransitionMessage, error) { + c.mu.Lock() + record, ok := c.attempts[handle.id] + if !ok { + c.mu.Unlock() + return nil, ErrUnknownAttempt + } + if c.selfMember == 0 || record.coordinator != c.selfMember { + c.mu.Unlock() + return nil, ErrNotAggregator + } + if record.state != AttemptStateCollecting { + c.mu.Unlock() + return nil, fmt.Errorf( + "%w: state is %v, want %v", + ErrAttemptStateInvalid, + record.state, + AttemptStateCollecting, + ) + } + + senders := make([]group.MemberIndex, 0, len(record.snapshots)) + for s := range record.snapshots { + senders = append(senders, s) + } + sort.Slice(senders, func(i, j int) bool { return senders[i] < senders[j] }) + + bundle := make([]LocalEvidenceSnapshot, 0, len(senders)) + for _, s := range senders { + bundle = append(bundle, *record.snapshots[s]) + } + + record.state = AttemptStateAggregating + hash := record.handle.contextHash + coord := record.coordinator + c.mu.Unlock() + + msg := &TransitionMessage{ + AttemptContextHash: append([]byte{}, hash[:]...), + CoordinatorIDValue: uint32(coord), + Bundle: bundle, + } + payload, err := CanonicalBundleBytes(msg) + if err != nil { + c.markTransitionedLocked(handle.id) + return nil, fmt.Errorf("coordinator: canonical bundle: %w", err) + } + sig, err := c.signer.Sign(payload) + if err != nil { + c.markTransitionedLocked(handle.id) + return nil, fmt.Errorf("coordinator: sign bundle: %w", err) + } + msg.CoordinatorSignature = sig + if err := msg.Validate(); err != nil { + c.markTransitionedLocked(handle.id) + return nil, fmt.Errorf("coordinator: aggregated bundle invalid: %w", err) + } + c.markTransitionedLocked(handle.id) + return msg, nil +} + +func (c *inMemoryCoordinator) markTransitionedLocked(id uint64) { + c.mu.Lock() + defer c.mu.Unlock() + if record, ok := c.attempts[id]; ok { + record.state = AttemptStateTransitioned + } +} + +func (c *inMemoryCoordinator) VerifyBundle( + handle AttemptHandle, + msg *TransitionMessage, +) error { + if msg == nil { + return errors.New("coordinator: transition message is nil") + } + if err := msg.Validate(); err != nil { + return fmt.Errorf("coordinator: transition message invalid: %w", err) + } + + c.mu.Lock() + record, ok := c.attempts[handle.id] + if !ok { + c.mu.Unlock() + return ErrUnknownAttempt + } + expectedCoordinator := record.coordinator + expectedHash := record.handle.contextHash + selfSubmission := record.selfSubmission + c.mu.Unlock() + + if !bytes.Equal(msg.AttemptContextHash, expectedHash[:]) { + return ErrAttemptContextMismatch + } + if err := verifyBundleSignature(c.verifier, msg, expectedCoordinator); err != nil { + return fmt.Errorf("coordinator: %w", err) + } + for i := range msg.Bundle { + if err := verifySnapshotSignature(c.verifier, &msg.Bundle[i]); err != nil { + return fmt.Errorf("coordinator: bundle[%d]: %w", i, err) + } + } + if err := verifyOwnObservationsPresent(msg, c.selfMember, selfSubmission); err != nil { + return err + } + return nil +} diff --git a/pkg/frost/roast/coordinator_state_test.go b/pkg/frost/roast/coordinator_state_test.go new file mode 100644 index 0000000000..0fdc0afb5c --- /dev/null +++ b/pkg/frost/roast/coordinator_state_test.go @@ -0,0 +1,263 @@ +package roast + +import ( + "errors" + "sync" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func newTestContext(t *testing.T) attempt.AttemptContext { + t.Helper() + ctx, err := attempt.NewAttemptContext( + "session-test", + "key-group-test", + []byte{0xab, 0xcd, 0xef}, + [attempt.MessageDigestLength]byte{0x42}, + 0, + []group.MemberIndex{1, 2, 3, 4, 5}, + nil, + ) + if err != nil { + t.Fatalf("test context: %v", err) + } + return ctx +} + +func TestBeginAttempt_ReturnsHandleWithMatchingContextHash(t *testing.T) { + coord := NewInMemoryCoordinator() + ctx := newTestContext(t) + handle, err := coord.BeginAttempt(ctx) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if handle.ContextHash() != ctx.Hash() { + t.Fatalf( + "handle hash mismatch: got %x want %x", + handle.ContextHash(), ctx.Hash(), + ) + } +} + +func TestBeginAttempt_HandlesAreDistinctAcrossAttempts(t *testing.T) { + coord := NewInMemoryCoordinator() + a, err := coord.BeginAttempt(newTestContext(t)) + if err != nil { + t.Fatalf("first begin: %v", err) + } + b, err := coord.BeginAttempt(newTestContext(t)) + if err != nil { + t.Fatalf("second begin: %v", err) + } + if a.id == b.id { + t.Fatalf("two attempts shared handle id %d", a.id) + } +} + +func TestBeginAttempt_RejectsEmptyIncludedSet(t *testing.T) { + coord := NewInMemoryCoordinator() + // We bypass NewAttemptContext (which forbids empty included set) + // to assert BeginAttempt's defence-in-depth check. + ctx := attempt.AttemptContext{} + _, err := coord.BeginAttempt(ctx) + if err == nil { + t.Fatal("expected error on empty included set") + } +} + +func TestState_ReturnsCollectingAfterBegin(t *testing.T) { + coord := NewInMemoryCoordinator() + handle, err := coord.BeginAttempt(newTestContext(t)) + if err != nil { + t.Fatalf("begin: %v", err) + } + state, err := coord.State(handle) + if err != nil { + t.Fatalf("state: %v", err) + } + if state != AttemptStateCollecting { + t.Fatalf( + "expected collecting, got %v", + state, + ) + } +} + +func TestState_UnknownHandleReturnsSentinel(t *testing.T) { + coord := NewInMemoryCoordinator() + bogus := AttemptHandle{id: 999} + state, err := coord.State(bogus) + if !errors.Is(err, ErrUnknownAttempt) { + t.Fatalf("expected ErrUnknownAttempt, got %v", err) + } + if state != AttemptStatePending { + t.Fatalf("expected pending sentinel, got %v", state) + } +} + +func TestSelectedCoordinator_ReturnsMemberFromIncludedSet(t *testing.T) { + coord := NewInMemoryCoordinator() + ctx := newTestContext(t) + handle, err := coord.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + got, err := coord.SelectedCoordinator(handle) + if err != nil { + t.Fatalf("selected coordinator: %v", err) + } + found := false + for _, m := range ctx.IncludedSet { + if m == got { + found = true + break + } + } + if !found { + t.Fatalf( + "selected coordinator %d not in included set %v", + got, ctx.IncludedSet, + ) + } +} + +func TestSelectedCoordinator_IsDeterministicForSameContext(t *testing.T) { + a := NewInMemoryCoordinator() + b := NewInMemoryCoordinator() + ctx := newTestContext(t) + ha, err := a.BeginAttempt(ctx) + if err != nil { + t.Fatalf("a.begin: %v", err) + } + hb, err := b.BeginAttempt(ctx) + if err != nil { + t.Fatalf("b.begin: %v", err) + } + ca, err := a.SelectedCoordinator(ha) + if err != nil { + t.Fatalf("a.selected: %v", err) + } + cb, err := b.SelectedCoordinator(hb) + if err != nil { + t.Fatalf("b.selected: %v", err) + } + if ca != cb { + t.Fatalf( + "two coordinators disagreed on same context: %d != %d", + ca, cb, + ) + } +} + +func TestSelectedCoordinator_DifferentAttemptNumbersCanProduceDifferentLeaders(t *testing.T) { + coord := NewInMemoryCoordinator() + build := func(attemptNumber uint32) attempt.AttemptContext { + ctx, err := attempt.NewAttemptContext( + "session-test", + "key-group-test", + []byte{0x01}, + [attempt.MessageDigestLength]byte{0x42}, + attemptNumber, + []group.MemberIndex{1, 2, 3, 4, 5}, + nil, + ) + if err != nil { + t.Fatalf("build ctx: %v", err) + } + return ctx + } + + // Sweep a few attempt numbers; verify the elected coordinator is + // not always the same member -- otherwise the retry-rotation + // property of ROAST does not hold. + seen := map[group.MemberIndex]struct{}{} + for n := uint32(0); n < 16; n++ { + ctx := build(n) + handle, err := coord.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin n=%d: %v", n, err) + } + c, err := coord.SelectedCoordinator(handle) + if err != nil { + t.Fatalf("selected n=%d: %v", n, err) + } + seen[c] = struct{}{} + } + if len(seen) < 2 { + t.Fatalf( + "coordinator rotation broken: 16 different attempts all "+ + "elected the same leader; seen=%v", + seen, + ) + } +} + +func TestSelectedCoordinator_UnknownHandleReturnsSentinel(t *testing.T) { + coord := NewInMemoryCoordinator() + bogus := AttemptHandle{id: 999} + got, err := coord.SelectedCoordinator(bogus) + if !errors.Is(err, ErrUnknownAttempt) { + t.Fatalf("expected ErrUnknownAttempt, got %v", err) + } + if got != 0 { + t.Fatalf("expected zero member index, got %d", got) + } +} + +func TestInMemoryCoordinator_ConcurrentBeginAttemptsAreRaceSafe(t *testing.T) { + const numGoroutines = 16 + const beginsPerGoroutine = 50 + + coord := NewInMemoryCoordinator() + var wg sync.WaitGroup + handles := make(chan AttemptHandle, numGoroutines*beginsPerGoroutine) + + for g := 0; g < numGoroutines; g++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < beginsPerGoroutine; i++ { + h, err := coord.BeginAttempt(newTestContext(t)) + if err != nil { + t.Errorf("concurrent begin: %v", err) + return + } + handles <- h + } + }() + } + wg.Wait() + close(handles) + + ids := map[uint64]struct{}{} + for h := range handles { + if _, dup := ids[h.id]; dup { + t.Fatalf("duplicate handle id %d under concurrency", h.id) + } + ids[h.id] = struct{}{} + } + if len(ids) != numGoroutines*beginsPerGoroutine { + t.Fatalf( + "expected %d unique handles, got %d", + numGoroutines*beginsPerGoroutine, len(ids), + ) + } +} + +func TestAttemptState_String(t *testing.T) { + cases := map[AttemptState]string{ + AttemptStatePending: "pending", + AttemptStateCollecting: "collecting", + AttemptStateAggregating: "aggregating", + AttemptStateSucceeded: "succeeded", + AttemptStateTransitioned: "transitioned", + AttemptState(99): "unknown(99)", + } + for state, want := range cases { + if got := state.String(); got != want { + t.Errorf("State %d: got %q want %q", state, got, want) + } + } +} 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/roast/multi_coordinator_soak_test.go b/pkg/frost/roast/multi_coordinator_soak_test.go new file mode 100644 index 0000000000..cd7dbc9b95 --- /dev/null +++ b/pkg/frost/roast/multi_coordinator_soak_test.go @@ -0,0 +1,430 @@ +package roast + +import ( + "bytes" + "crypto/sha256" + "errors" + "sort" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// The soak harness models the production deployment: every signer +// runs its own Coordinator instance bound to its own selfMember, +// shares the same signer/verifier scheme (here a deterministic +// SHA-256 stand-in), and must compute byte-identical next contexts +// given the same verified TransitionMessage. +// +// The harness exercises RFC-21 Layer A (overflow exclusion), Layer +// B (silence parking + reinstatement), and the policy's +// infeasibility floor under synthetic fault injection. The receive +// loops are bypassed -- they are unit-tested elsewhere; what the +// soak harness adds is the multi-instance-agreement property. + +// soakSigner produces SHA-256(member || payload) signatures. The +// matching soakVerifier accepts any signature byte-identical to +// the recomputation, so cross-instance verification works without +// real crypto. +type soakSigner struct { + id group.MemberIndex +} + +func (s *soakSigner) Sign(payload []byte) ([]byte, error) { + h := sha256.New() + h.Write([]byte{byte(s.id)}) + h.Write(payload) + return h.Sum(nil), nil +} + +type soakVerifier struct{} + +func (soakVerifier) Verify(payload, signature []byte, signer group.MemberIndex) error { + h := sha256.New() + h.Write([]byte{byte(signer)}) + h.Write(payload) + want := h.Sum(nil) + if !bytes.Equal(want, signature) { + return errors.New("soakVerifier: signature does not match recomputation") + } + return nil +} + +// soakNode bundles one signer's Coordinator instance, its self +// signer, and the snapshot it submits each attempt. +type soakNode struct { + self group.MemberIndex + coord Coordinator + signer *soakSigner +} + +// newSoakHarness initialises N coordinator instances bound to +// member indices 1..N, ready to BeginAttempt against a shared +// AttemptContext. Returns the nodes plus a deterministic +// shared-state baseline attempt context. +func newSoakHarness( + t *testing.T, + members []group.MemberIndex, +) []*soakNode { + t.Helper() + nodes := make([]*soakNode, 0, len(members)) + for _, m := range members { + signer := &soakSigner{id: m} + node := &soakNode{ + self: m, + coord: NewInMemoryCoordinatorWithSigning(m, signer, soakVerifier{}), + signer: signer, + } + nodes = append(nodes, node) + } + return nodes +} + +// soakAttempt drives a full attempt across every node: +// +// 1. Every node calls BeginAttempt with the shared context. +// 2. Every node produces a signed snapshot per the fault map +// (silent members produce nil; overflowing members produce +// snapshots with overflow events). +// 3. Every node receives every other node's snapshot via +// RecordEvidence. +// 4. The elected coordinator's node calls AggregateBundle. +// 5. Every non-coordinator node calls VerifyBundle. +// 6. Every node calls NextAttempt against the same verified +// bundle. +// +// Returns the next AttemptContext computed by every node (all must +// be byte-identical) and the elected coordinator's identity for +// the *current* attempt. +// +// silenceFor and overflowFor are maps that let the test inject +// faults. overflowFor[observer] = [senders the observer reports +// having overflowed]. +func soakAttempt( + t *testing.T, + nodes []*soakNode, + ctx attempt.AttemptContext, + silenceFor map[group.MemberIndex]bool, + overflowFor map[group.MemberIndex][]group.MemberIndex, + threshold uint, +) (attempt.AttemptContext, group.MemberIndex) { + t.Helper() + + type beginResult struct { + node *soakNode + handle AttemptHandle + } + begins := make([]beginResult, 0, len(nodes)) + for _, n := range nodes { + h, err := n.coord.BeginAttempt(ctx) + if err != nil { + t.Fatalf("node %d BeginAttempt: %v", n.self, err) + } + begins = append(begins, beginResult{node: n, handle: h}) + } + + // Elect coordinator: each node has the same SelectCoordinator + // result for this context, so it doesn't matter which node we + // ask. Use begins[0]. + elected, err := begins[0].node.coord.SelectedCoordinator(begins[0].handle) + if err != nil { + t.Fatalf("SelectedCoordinator: %v", err) + } + + // Each node produces a snapshot unless silent. + type signedSnap struct { + from group.MemberIndex + snapshot *LocalEvidenceSnapshot + } + snaps := make([]signedSnap, 0, len(nodes)) + for _, n := range nodes { + if silenceFor[n.self] { + continue + } + evidence := attempt.Evidence{ + Overflows: map[group.MemberIndex]uint{}, + } + for _, sender := range overflowFor[n.self] { + evidence.Overflows[sender]++ + } + snap := NewLocalEvidenceSnapshot(n.self, ctx.Hash(), evidence) + payload, _ := CanonicalSnapshotBytes(snap) + sig, _ := n.signer.Sign(payload) + snap.OperatorSignature = sig + snaps = append(snaps, signedSnap{from: n.self, snapshot: snap}) + } + + // Every node receives every snapshot. + for _, b := range begins { + for _, s := range snaps { + if err := b.node.coord.RecordEvidence(b.handle, s.snapshot); err != nil { + t.Fatalf( + "node %d RecordEvidence from %d: %v", + b.node.self, s.from, err, + ) + } + } + } + + // Find the elected coordinator's node and aggregate. + var aggregator beginResult + for _, b := range begins { + if b.node.self == elected { + aggregator = b + break + } + } + if aggregator.node == nil { + t.Fatalf("elected coordinator %d not in nodes", elected) + } + bundle, err := aggregator.node.coord.AggregateBundle(aggregator.handle) + if err != nil { + t.Fatalf("AggregateBundle on elected node %d: %v", elected, err) + } + + // Every non-coordinator node verifies the bundle. + for _, b := range begins { + if b.node.self == elected { + continue + } + if err := b.node.coord.VerifyBundle(b.handle, bundle); err != nil { + t.Fatalf("node %d VerifyBundle: %v", b.node.self, err) + } + } + + // Every node computes NextAttempt. + dkgPub := []byte{0xab, 0xcd, 0xef} + nextContexts := make([]attempt.AttemptContext, 0, len(nodes)) + for _, b := range begins { + next, err := b.node.coord.NextAttempt( + b.handle, + bundle, + threshold, + dkgPub, + ) + if err != nil { + t.Fatalf("node %d NextAttempt: %v", b.node.self, err) + } + nextContexts = append(nextContexts, next) + } + + // All nodes must produce byte-identical next contexts. + for i := 1; i < len(nextContexts); i++ { + if nextContexts[i].Hash() != nextContexts[0].Hash() { + t.Fatalf( + "multi-instance agreement violated: node 0 hash %x, node %d hash %x", + nextContexts[0].Hash(), + i, + nextContexts[i].Hash(), + ) + } + } + + return nextContexts[0], elected +} + +func soakStartingContext( + t *testing.T, + included []group.MemberIndex, +) attempt.AttemptContext { + t.Helper() + ctx, err := attempt.NewAttemptContext( + "soak-session", + "soak-key-group", + []byte{0xab, 0xcd, 0xef}, + [attempt.MessageDigestLength]byte{0x99}, + 0, + included, + nil, + ) + if err != nil { + t.Fatalf("starting ctx: %v", err) + } + return ctx +} + +func TestSoak_CleanAttemptPreservesIncludedSet(t *testing.T) { + members := []group.MemberIndex{1, 2, 3, 4, 5} + nodes := newSoakHarness(t, members) + prev := soakStartingContext(t, members) + + next, _ := soakAttempt(t, nodes, prev, nil, nil, 3) + + if len(next.IncludedSet) != len(members) { + t.Fatalf( + "clean attempt must preserve IncludedSet size; got %d want %d", + len(next.IncludedSet), len(members), + ) + } + if len(next.ExcludedSet) != 0 { + t.Fatalf("clean attempt must not exclude anyone; got %v", next.ExcludedSet) + } + if len(next.TransientlyParked) != 0 { + t.Fatalf("clean attempt must not park anyone; got %v", next.TransientlyParked) + } +} + +func TestSoak_OverflowEvidenceExcludesPermanently(t *testing.T) { + members := []group.MemberIndex{1, 2, 3, 4, 5} + nodes := newSoakHarness(t, members) + prev := soakStartingContext(t, members) + + // Four observers report 1 overflow each against member 3. + // Total 4 = OverflowExclusionThreshold. + overflow := map[group.MemberIndex][]group.MemberIndex{ + 1: {3}, + 2: {3}, + 4: {3}, + 5: {3}, + } + next, _ := soakAttempt(t, nodes, prev, nil, overflow, 3) + + if !containsMember(next.ExcludedSet, 3) { + t.Fatalf("member 3 must be excluded; got %v", next.ExcludedSet) + } + if containsMember(next.IncludedSet, 3) { + t.Fatal("member 3 must not be in next IncludedSet") + } +} + +func TestSoak_SilenceParksTransiently(t *testing.T) { + members := []group.MemberIndex{1, 2, 3, 4, 5} + nodes := newSoakHarness(t, members) + prev := soakStartingContext(t, members) + + silence := map[group.MemberIndex]bool{3: true} + next, _ := soakAttempt(t, nodes, prev, silence, nil, 3) + + if !containsMember(next.TransientlyParked, 3) { + t.Fatalf("silent member 3 must be parked; got %v", next.TransientlyParked) + } + if containsMember(next.ExcludedSet, 3) { + t.Fatal("silent member 3 must not be permanently excluded") + } + if containsMember(next.IncludedSet, 3) { + t.Fatal("silent member 3 must not be in next IncludedSet") + } +} + +func TestSoak_ParkedMemberIsReinstatedNextAttempt(t *testing.T) { + members := []group.MemberIndex{1, 2, 3, 4, 5} + nodes := newSoakHarness(t, members) + prev := soakStartingContext(t, members) + + // Attempt N: member 3 silent → parked at N+1. + silenceN := map[group.MemberIndex]bool{3: true} + contextN1, _ := soakAttempt(t, nodes, prev, silenceN, nil, 3) + if !containsMember(contextN1.TransientlyParked, 3) { + t.Fatalf("setup: N+1 must park member 3; got %v", contextN1.TransientlyParked) + } + + // Attempt N+1: member 3 cannot submit (parked). Other 4 members + // do submit. Need a fresh harness because each node's + // Coordinator already transitioned its previous attempt. + nextNodes := newSoakHarness(t, members) + silenceN1 := map[group.MemberIndex]bool{ + 3: true, // parked by design, cannot submit + } + contextN2, _ := soakAttempt(t, nextNodes, contextN1, silenceN1, nil, 3) + + if !containsMember(contextN2.IncludedSet, 3) { + t.Fatalf("member 3 must be reinstated at N+2; got %v", contextN2.IncludedSet) + } + if containsMember(contextN2.TransientlyParked, 3) { + t.Fatal("member 3 must not be re-parked at N+2") + } + if containsMember(contextN2.ExcludedSet, 3) { + t.Fatal("member 3 must not be permanently excluded at N+2") + } +} + +func TestSoak_InfeasibilityWhenBelowThreshold(t *testing.T) { + members := []group.MemberIndex{1, 2, 3, 4, 5} + nodes := newSoakHarness(t, members) + prev := soakStartingContext(t, members) + + // Threshold = 5 (all members required). Silence two members. + // Next attempt's IncludedSet would be 3 (= 5 - 2 silenced), below 5. + // NextAttempt must return ErrAttemptInfeasible. + silence := map[group.MemberIndex]bool{ + 4: true, + 5: true, + } + // Build the bundle manually because soakAttempt panics on + // NextAttempt error. Walk the same steps but skip the post- + // aggregate verify on infeasibility. + type beginResult struct { + node *soakNode + handle AttemptHandle + } + begins := make([]beginResult, 0, len(nodes)) + for _, n := range nodes { + h, _ := n.coord.BeginAttempt(prev) + begins = append(begins, beginResult{node: n, handle: h}) + } + for _, n := range nodes { + if silence[n.self] { + continue + } + snap := NewLocalEvidenceSnapshot(n.self, prev.Hash(), attempt.Evidence{}) + payload, _ := CanonicalSnapshotBytes(snap) + sig, _ := n.signer.Sign(payload) + snap.OperatorSignature = sig + for _, b := range begins { + _ = b.node.coord.RecordEvidence(b.handle, snap) + } + } + elected, _ := begins[0].node.coord.SelectedCoordinator(begins[0].handle) + var aggregator beginResult + for _, b := range begins { + if b.node.self == elected { + aggregator = b + break + } + } + bundle, _ := aggregator.node.coord.AggregateBundle(aggregator.handle) + + // Verify each non-coordinator's NextAttempt returns infeasible. + for _, b := range begins { + _, err := b.node.coord.NextAttempt(b.handle, bundle, 5, []byte{0x01}) + if !errors.Is(err, ErrAttemptInfeasible) { + t.Fatalf( + "node %d NextAttempt: expected ErrAttemptInfeasible; got %v", + b.node.self, err, + ) + } + } +} + +func TestSoak_OriginalSignerSetIsPreservedAcrossThreeTransitions(t *testing.T) { + members := []group.MemberIndex{1, 2, 3, 4, 5} + prev := soakStartingContext(t, members) + + // Three attempts back-to-back, with fresh harnesses each + // (real signers run one attempt per Coordinator instance). + for i := 0; i < 3; i++ { + nodes := newSoakHarness(t, members) + next, _ := soakAttempt(t, nodes, prev, nil, nil, 3) + if sz := len(next.IncludedSet) + len(next.ExcludedSet) + len(next.TransientlyParked); sz != len(members) { + t.Fatalf( + "attempt %d: |Inc|+|Exc|+|Park| = %d, want %d", + i, sz, len(members), + ) + } + prev = next + } +} + +func containsMember(slice []group.MemberIndex, target group.MemberIndex) bool { + for _, m := range slice { + if m == target { + return true + } + } + return false +} + +// silence the unused-import warning for sort if no test references +// it directly. +var _ = sort.Slice diff --git a/pkg/frost/roast/next_attempt.go b/pkg/frost/roast/next_attempt.go new file mode 100644 index 0000000000..4f896c6b23 --- /dev/null +++ b/pkg/frost/roast/next_attempt.go @@ -0,0 +1,331 @@ +package roast + +import ( + "errors" + "fmt" + "sort" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// OverflowExclusionThreshold is the per-sender overflow-count +// threshold above which the NextAttempt policy permanently excludes +// the sender (transport-blamable). Matches the constant documented in +// RFC-21 Layer B. +const OverflowExclusionThreshold uint = 4 + +// RejectExclusionThreshold is the per-sender summed-reject-count +// threshold above which the NextAttempt policy permanently +// excludes the sender (validation-blamable). RFC-21 Layer B +// specifies any non-transport reject as sufficient cause, so the +// constant is 1. Reasons are not differentiated by the policy +// today; every reject category counts equally. +const RejectExclusionThreshold uint = 1 + +// ConflictExclusionThreshold is the per-sender summed-conflict- +// count threshold above which the NextAttempt policy permanently +// excludes the sender (equivocation-blamable). A single +// first-write-wins conflict is sufficient evidence: an honest +// peer retransmitting a contribution sends byte-identical bytes, +// so a conflict implies the peer changed its claim mid-attempt. +const ConflictExclusionThreshold uint = 1 + +// ErrAttemptInfeasible is returned by NextAttempt when the next +// attempt's IncludedSet would drop below the signing threshold t and +// the session can no longer make progress with the original signer +// set. Callers must surface this to the application layer: the +// session is permanently failed. +var ErrAttemptInfeasible = errors.New( + "coordinator: next attempt is infeasible -- included set below threshold", +) + +// NextAttempt computes the deterministic next attempt context from a +// verified TransitionMessage. It is a pure function of +// (previous AttemptContext, bundle, threshold): two honest signers +// fed the same inputs produce byte-identical outputs, so the +// signing-group state machine remains in agreement across the +// network. +// +// Callers MUST call VerifyBundle on the message before passing it to +// NextAttempt. NextAttempt does not re-run the signature checks; it +// assumes the bundle is verified and only applies the policy. +// +// The policy (RFC-21 Layer B): +// +// 1. Permanent exclusion (transport-blamable): a sender whose total +// overflow count across the bundle is at least +// OverflowExclusionThreshold is added to ExcludedSet forever. +// +// 2. Permanent exclusion (validation-blamable): senders with +// confirmed non-transport reject events. Phase 3.4 does not yet +// track reject events, so this is a no-op; the hook is in place +// for a later phase. +// +// 3. Silence parking (strictly transient): a sender in the +// previous attempt's IncludedSet that does not appear in the +// bundle, and is not permanently excluded, is added to the next +// attempt's TransientlyParked set. The attempt after that +// automatically reinstates them, so a falsely-silenced honest +// peer recovers without intervention. +// +// 4. Reinstatement: members in the previous attempt's +// TransientlyParked set automatically rejoin the next attempt's +// IncludedSet (unless they are now permanently excluded for +// another reason). +// +// 5. Infeasibility: if the next attempt's IncludedSet would have +// fewer than threshold members, return ErrAttemptInfeasible. +// +// threshold is the FROST signing threshold t for the key group; it +// is constant across attempts within a session. A threshold of zero +// disables the infeasibility check (useful in tests that exercise +// the policy independently from threshold semantics). +// +// The caller is responsible for supplying the DKG group public key +// from the same source the previous attempt used (the FFI signer +// material, per RFC-21 Decision 2); a different source would +// silently desynchronise the seed derivation. +func (c *inMemoryCoordinator) NextAttempt( + handle AttemptHandle, + bundle *TransitionMessage, + threshold uint, + dkgGroupPublicKey []byte, +) (attempt.AttemptContext, error) { + if bundle == nil { + return attempt.AttemptContext{}, errors.New( + "coordinator: cannot compute next attempt from nil bundle", + ) + } + c.mu.Lock() + record, ok := c.attempts[handle.id] + if !ok { + c.mu.Unlock() + return attempt.AttemptContext{}, ErrUnknownAttempt + } + prev := record.context + c.mu.Unlock() + + return computeNextAttempt(prev, bundle, threshold, dkgGroupPublicKey) +} + +// computeNextAttempt is the pure-function policy core: it takes the +// previous AttemptContext, a verified bundle, and the signing +// threshold, and returns the next AttemptContext. Factored out from +// NextAttempt so the policy is independently unit-testable without a +// Coordinator instance. +func computeNextAttempt( + prev attempt.AttemptContext, + bundle *TransitionMessage, + threshold uint, + dkgGroupPublicKey []byte, +) (attempt.AttemptContext, error) { + // (1) Permanent exclusion from overflow evidence (transport + // blamable). + overflowBlamed := overflowBlamedSenders(bundle, OverflowExclusionThreshold) + + // (2) Permanent exclusion from reject evidence (validation + // blamable). Counts across reasons are summed per-sender. + rejectBlamed := rejectBlamedSenders(bundle, RejectExclusionThreshold) + + // (3) Permanent exclusion from conflict evidence (equivocation + // blamable). First-write-wins disagreements by the same + // sender within an attempt are taken as proof of byzantine + // behaviour. + conflictBlamed := conflictBlamedSenders(bundle, ConflictExclusionThreshold) + + // Merge into permanent exclusion. + exclSet := newMemberSet() + exclSet.addAll(prev.ExcludedSet) + exclSet.addAll(overflowBlamed) + exclSet.addAll(rejectBlamed) + exclSet.addAll(conflictBlamed) + + // (3) Silence parking: senders in prev.IncludedSet but not in + // bundle, that we are not now permanently excluding. + bundleSenders := bundleSenderSet(bundle) + parkSet := newMemberSet() + for _, m := range prev.IncludedSet { + if bundleSenders.contains(m) { + continue + } + if exclSet.contains(m) { + continue + } + parkSet.add(m) + } + + // (4) Original signer set persists across transitions as + // IncludedSet ∪ ExcludedSet ∪ TransientlyParked. Reinstate + // previously parked members by re-including them + // (unless newly permanently excluded -- which they cannot be, + // since they could not have submitted overflow evidence + // this attempt). + original := newMemberSet() + original.addAll(prev.IncludedSet) + original.addAll(prev.ExcludedSet) + original.addAll(prev.TransientlyParked) + + included := original.sorted() + included = filterOut(included, exclSet) + included = filterOut(included, parkSet) + + // (5) Infeasibility check. + if threshold > 0 && uint(len(included)) < threshold { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: %d eligible, threshold %d", + ErrAttemptInfeasible, + len(included), + threshold, + ) + } + + // Convert ExcludedSet to its canonical (sorted, deduped) slice. + nextExcluded := exclSet.sorted() + nextParked := parkSet.sorted() + + next, err := attempt.NewAttemptContextWithParking( + prev.SessionID, + prev.KeyGroupID, + dkgGroupPublicKey, + prev.MessageDigest, + prev.AttemptNumber+1, + included, + nextExcluded, + nextParked, + ) + if err != nil { + return attempt.AttemptContext{}, fmt.Errorf( + "coordinator: next attempt construction: %w", + err, + ) + } + return next, nil +} + +// overflowBlamedSenders returns the senders whose total overflow +// count across every snapshot in the bundle is at least the +// supplied threshold. Counts are summed (not averaged) so a sender +// hitting the threshold from one observer alone is sufficient. +func overflowBlamedSenders( + bundle *TransitionMessage, + threshold uint, +) []group.MemberIndex { + counts := map[group.MemberIndex]uint{} + for i := range bundle.Bundle { + for _, entry := range bundle.Bundle[i].Overflows { + counts[entry.Sender] += entry.Count + } + } + return blamedSenders(counts, threshold) +} + +// rejectBlamedSenders returns the senders whose total reject count +// (summed across all observers AND across all rejection reasons) +// meets the supplied threshold. Reasons are not differentiated at +// the policy layer; the recorder bounds per-reason quotas +// separately so a peer cannot spam one reason to mask another. +func rejectBlamedSenders( + bundle *TransitionMessage, + threshold uint, +) []group.MemberIndex { + counts := map[group.MemberIndex]uint{} + for i := range bundle.Bundle { + for _, entry := range bundle.Bundle[i].Rejects { + counts[entry.Sender] += entry.Count + } + } + return blamedSenders(counts, threshold) +} + +// conflictBlamedSenders returns the senders whose total +// first-write-wins-conflict count across the bundle meets the +// supplied threshold. A single conflict suffices under the +// default ConflictExclusionThreshold (= 1) because an honest peer +// retransmitting always sends byte-identical bytes. +func conflictBlamedSenders( + bundle *TransitionMessage, + threshold uint, +) []group.MemberIndex { + counts := map[group.MemberIndex]uint{} + for i := range bundle.Bundle { + for _, entry := range bundle.Bundle[i].Conflicts { + counts[entry.Sender] += entry.Count + } + } + return blamedSenders(counts, threshold) +} + +// blamedSenders extracts the deterministically-sorted list of +// senders whose accumulated count meets the threshold. Factored +// out so the three category helpers share the same canonicalisation. +func blamedSenders( + counts map[group.MemberIndex]uint, + threshold uint, +) []group.MemberIndex { + out := make([]group.MemberIndex, 0) + for sender, count := range counts { + if count >= threshold { + out = append(out, sender) + } + } + sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) + return out +} + +// bundleSenderSet returns the set of senders that submitted a +// snapshot to the bundle. +func bundleSenderSet(bundle *TransitionMessage) *memberSet { + out := newMemberSet() + for i := range bundle.Bundle { + out.add(bundle.Bundle[i].SenderID()) + } + return out +} + +// memberSet is a small helper for set arithmetic over +// group.MemberIndex. Sufficient for the small (≤256) sizes the +// coordinator deals with. +type memberSet struct { + m map[group.MemberIndex]struct{} +} + +func newMemberSet() *memberSet { + return &memberSet{m: map[group.MemberIndex]struct{}{}} +} + +func (s *memberSet) add(member group.MemberIndex) { s.m[member] = struct{}{} } +func (s *memberSet) contains(m group.MemberIndex) bool { + _, ok := s.m[m] + return ok +} + +func (s *memberSet) addAll(members []group.MemberIndex) { + for _, m := range members { + s.add(m) + } +} + +func (s *memberSet) sorted() []group.MemberIndex { + out := make([]group.MemberIndex, 0, len(s.m)) + for m := range s.m { + out = append(out, m) + } + sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) + return out +} + +// filterOut returns members not in the excluded set, preserving +// input order. +func filterOut( + members []group.MemberIndex, + excluded *memberSet, +) []group.MemberIndex { + out := make([]group.MemberIndex, 0, len(members)) + for _, m := range members { + if !excluded.contains(m) { + out = append(out, m) + } + } + return out +} diff --git a/pkg/frost/roast/next_attempt_categories_test.go b/pkg/frost/roast/next_attempt_categories_test.go new file mode 100644 index 0000000000..0729ae13e6 --- /dev/null +++ b/pkg/frost/roast/next_attempt_categories_test.go @@ -0,0 +1,165 @@ +package roast + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// buildBundleWithCategories constructs a TransitionMessage where each +// observer contributes the same per-(category, sender) evidence -- one +// reject reason and one conflict per "blamed" sender per observer. +// Useful for verifying the cross-observer summing behaviour. +func buildBundleWithCategories( + t *testing.T, + prev attempt.AttemptContext, + rejects map[group.MemberIndex][]string, + conflicts []group.MemberIndex, +) *TransitionMessage { + t.Helper() + prevHash := prev.Hash() + bundle := make([]LocalEvidenceSnapshot, 0, len(prev.IncludedSet)) + for _, sender := range prev.IncludedSet { + snap := LocalEvidenceSnapshot{ + SenderIDValue: uint32(sender), + AttemptContextHash: append([]byte{}, prevHash[:]...), + } + var rejectEntries []RejectEntry + for blamedSender, reasons := range rejects { + for _, r := range reasons { + rejectEntries = append(rejectEntries, RejectEntry{ + Sender: blamedSender, + Reason: r, + Count: 1, + }) + } + } + sortRejectEntriesForTest(rejectEntries) + if len(rejectEntries) > 0 { + snap.Rejects = rejectEntries + } + var conflictEntries []ConflictEntry + for _, blamedSender := range conflicts { + conflictEntries = append(conflictEntries, ConflictEntry{ + Sender: blamedSender, + Count: 1, + }) + } + if len(conflictEntries) > 0 { + snap.Conflicts = conflictEntries + } + bundle = append(bundle, snap) + } + return &TransitionMessage{ + AttemptContextHash: append([]byte{}, prevHash[:]...), + CoordinatorIDValue: 1, + Bundle: bundle, + } +} + +func sortRejectEntriesForTest(entries []RejectEntry) { + for i := 1; i < len(entries); i++ { + for j := i; j > 0 && (entries[j].Sender < entries[j-1].Sender || + (entries[j].Sender == entries[j-1].Sender && entries[j].Reason < entries[j-1].Reason)); j-- { + entries[j], entries[j-1] = entries[j-1], entries[j] + } + } +} + +func TestNextAttempt_SingleRejectExcludesPermanently(t *testing.T) { + f := newNextAttemptFixture() + prev := f.prev(t) + // Every observer reports one reject against sender 3 → total + // count is len(IncludedSet) = 5 across observers, summed by + // rejectBlamedSenders. + bundle := buildBundleWithCategories( + t, + prev, + map[group.MemberIndex][]string{3: {"validation_gate_rejected"}}, + nil, + ) + + next, err := computeNextAttempt(prev, bundle, f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if !memberSliceContains(next.ExcludedSet, 3) { + t.Fatalf("sender 3 must be excluded; got %v", next.ExcludedSet) + } + if memberSliceContains(next.IncludedSet, 3) { + t.Fatal("sender 3 must not be in next IncludedSet") + } +} + +func TestNextAttempt_SingleConflictExcludesPermanently(t *testing.T) { + f := newNextAttemptFixture() + prev := f.prev(t) + bundle := buildBundleWithCategories( + t, + prev, + nil, + []group.MemberIndex{3}, + ) + + next, err := computeNextAttempt(prev, bundle, f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if !memberSliceContains(next.ExcludedSet, 3) { + t.Fatalf( + "sender 3 must be excluded after a single conflict; got %v", + next.ExcludedSet, + ) + } +} + +func TestNextAttempt_RejectAndConflictBothExclude(t *testing.T) { + f := newNextAttemptFixture() + prev := f.prev(t) + bundle := buildBundleWithCategories( + t, + prev, + map[group.MemberIndex][]string{2: {"validation_gate_rejected"}}, + []group.MemberIndex{4}, + ) + + next, err := computeNextAttempt(prev, bundle, f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if !memberSliceContains(next.ExcludedSet, 2) { + t.Fatalf("sender 2 (reject) must be excluded; got %v", next.ExcludedSet) + } + if !memberSliceContains(next.ExcludedSet, 4) { + t.Fatalf("sender 4 (conflict) must be excluded; got %v", next.ExcludedSet) + } +} + +func TestNextAttempt_EmptyRejectsAndConflicts_DoNotExclude(t *testing.T) { + f := newNextAttemptFixture() + prev := f.prev(t) + bundle := buildBundleWithCategories(t, prev, nil, nil) + next, err := computeNextAttempt(prev, bundle, f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if len(next.ExcludedSet) != 0 { + t.Fatalf("no evidence -> no exclusions; got %v", next.ExcludedSet) + } +} + +func TestRejectAndConflictThresholds_MatchRFC(t *testing.T) { + if RejectExclusionThreshold != 1 { + t.Fatalf( + "RFC-21 Layer B specifies reject threshold = 1; constant is %d", + RejectExclusionThreshold, + ) + } + if ConflictExclusionThreshold != 1 { + t.Fatalf( + "single conflict is sufficient evidence; constant is %d", + ConflictExclusionThreshold, + ) + } +} diff --git a/pkg/frost/roast/next_attempt_test.go b/pkg/frost/roast/next_attempt_test.go new file mode 100644 index 0000000000..47972dedd0 --- /dev/null +++ b/pkg/frost/roast/next_attempt_test.go @@ -0,0 +1,392 @@ +package roast + +import ( + "errors" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// nextAttemptFixture builds a previous AttemptContext and an +// associated TransitionMessage for the NextAttempt-policy tests. +// Members 1..5 included; no excluded; no parking. By default every +// member submits a snapshot with no overflow events. +type nextAttemptFixture struct { + included []group.MemberIndex + excluded []group.MemberIndex + parked []group.MemberIndex + overflows map[group.MemberIndex]map[group.MemberIndex]uint + bundleSenders []group.MemberIndex // override default = included + attemptNumber uint32 + dkgGroupPublicKey []byte + threshold uint + sessionID string + messageDigest [attempt.MessageDigestLength]byte +} + +func newNextAttemptFixture() *nextAttemptFixture { + return &nextAttemptFixture{ + included: []group.MemberIndex{1, 2, 3, 4, 5}, + excluded: nil, + parked: nil, + overflows: map[group.MemberIndex]map[group.MemberIndex]uint{}, + bundleSenders: nil, + attemptNumber: 0, + dkgGroupPublicKey: []byte{0x01, 0x02, 0x03}, + threshold: 3, + sessionID: "session-next-attempt", + messageDigest: [attempt.MessageDigestLength]byte{0x42}, + } +} + +func (f *nextAttemptFixture) prev(t *testing.T) attempt.AttemptContext { + t.Helper() + ctx, err := attempt.NewAttemptContextWithParking( + f.sessionID, + "key-group-next-attempt", + f.dkgGroupPublicKey, + f.messageDigest, + f.attemptNumber, + f.included, + f.excluded, + f.parked, + ) + if err != nil { + t.Fatalf("fixture prev: %v", err) + } + return ctx +} + +func (f *nextAttemptFixture) bundle(t *testing.T) *TransitionMessage { + t.Helper() + prev := f.prev(t) + prevHash := prev.Hash() + senders := f.bundleSenders + if senders == nil { + senders = append([]group.MemberIndex{}, f.included...) + } + bundle := make([]LocalEvidenceSnapshot, 0, len(senders)) + for _, s := range senders { + snap := LocalEvidenceSnapshot{ + SenderIDValue: uint32(s), + AttemptContextHash: append([]byte{}, prevHash[:]...), + } + if entries, ok := f.overflows[s]; ok { + ov := make([]OverflowEntry, 0, len(entries)) + for sender, count := range entries { + ov = append(ov, OverflowEntry{Sender: sender, Count: count}) + } + snap.Overflows = sortedOverflowEntries(ov) + } + bundle = append(bundle, snap) + } + return &TransitionMessage{ + AttemptContextHash: append([]byte{}, prevHash[:]...), + CoordinatorIDValue: 1, + Bundle: bundle, + } +} + +func sortedOverflowEntries(in []OverflowEntry) []OverflowEntry { + out := append([]OverflowEntry{}, in...) + // insertion sort; small slices. + for i := 1; i < len(out); i++ { + for j := i; j > 0 && out[j].Sender < out[j-1].Sender; j-- { + out[j], out[j-1] = out[j-1], out[j] + } + } + return out +} + +func TestNextAttempt_NoEvidenceProducesIdenticalIncludedSet(t *testing.T) { + f := newNextAttemptFixture() + prev := f.prev(t) + bundle := f.bundle(t) + next, err := computeNextAttempt(prev, bundle, f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if !memberSlicesEqual(next.IncludedSet, prev.IncludedSet) { + t.Fatalf( + "included set changed unexpectedly: prev=%v next=%v", + prev.IncludedSet, next.IncludedSet, + ) + } + if len(next.ExcludedSet) != 0 { + t.Fatalf("excluded set should be empty, got %v", next.ExcludedSet) + } + if len(next.TransientlyParked) != 0 { + t.Fatalf("parking set should be empty, got %v", next.TransientlyParked) + } + if next.AttemptNumber != prev.AttemptNumber+1 { + t.Fatalf( + "attempt number not incremented: got %d, want %d", + next.AttemptNumber, prev.AttemptNumber+1, + ) + } +} + +func TestNextAttempt_OverflowThresholdTriggersPermanentExclusion(t *testing.T) { + f := newNextAttemptFixture() + // Members 2..5 all report 1 overflow event each against sender 3. + // 4 observers × 1 event = 4 total = OverflowExclusionThreshold. + for observer := group.MemberIndex(2); observer <= 5; observer++ { + f.overflows[observer] = map[group.MemberIndex]uint{3: 1} + } + next, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if memberSliceContains(next.IncludedSet, 3) { + t.Fatalf("sender 3 should be excluded; got included %v", next.IncludedSet) + } + if !memberSliceContains(next.ExcludedSet, 3) { + t.Fatalf("sender 3 should appear in excluded set; got %v", next.ExcludedSet) + } +} + +func TestNextAttempt_OverflowBelowThresholdDoesNotExclude(t *testing.T) { + f := newNextAttemptFixture() + // Only 1 observer reports 1 overflow event against sender 3. + // 1 < threshold (4). + f.overflows[2] = map[group.MemberIndex]uint{3: 1} + next, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if !memberSliceContains(next.IncludedSet, 3) { + t.Fatalf("sender 3 should remain included; got %v", next.IncludedSet) + } +} + +func TestNextAttempt_SilentMemberIsParkedTransiently(t *testing.T) { + f := newNextAttemptFixture() + // Only members 1, 2, 4, 5 submit; member 3 is silent. + f.bundleSenders = []group.MemberIndex{1, 2, 4, 5} + next, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if memberSliceContains(next.IncludedSet, 3) { + t.Fatal("silent sender 3 must not appear in next IncludedSet") + } + if !memberSliceContains(next.TransientlyParked, 3) { + t.Fatalf("silent sender 3 must appear in next TransientlyParked; got %v", next.TransientlyParked) + } + if memberSliceContains(next.ExcludedSet, 3) { + t.Fatal("silent sender 3 must not be permanently excluded") + } +} + +func TestNextAttempt_PreviouslyParkedAreReinstated(t *testing.T) { + f := newNextAttemptFixture() + // Previous attempt: members 1, 2, 4, 5 included; member 3 parked. + f.included = []group.MemberIndex{1, 2, 4, 5} + f.parked = []group.MemberIndex{3} + // Bundle: only the included set submits (parked cannot). + f.bundleSenders = []group.MemberIndex{1, 2, 4, 5} + + next, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if !memberSliceContains(next.IncludedSet, 3) { + t.Fatalf( + "previously parked member 3 must be reinstated; got included %v", + next.IncludedSet, + ) + } + if memberSliceContains(next.TransientlyParked, 3) { + t.Fatal("member 3 must not be re-parked") + } + if memberSliceContains(next.ExcludedSet, 3) { + t.Fatal("member 3 must not be excluded") + } +} + +func TestNextAttempt_ParkingIsStrictlyTransient_NoEscalation(t *testing.T) { + // Demonstrate the full cycle: park, skip one attempt, reinstate. + // Attempt N: member 3 is silent. + // Attempt N+1: member 3 is parked, did not submit. + // Attempt N+2: member 3 is reinstated. + f := newNextAttemptFixture() + f.bundleSenders = []group.MemberIndex{1, 2, 4, 5} + prev := f.prev(t) + bundle := f.bundle(t) + attemptN1, err := computeNextAttempt(prev, bundle, f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("N -> N+1: %v", err) + } + if !memberSliceContains(attemptN1.TransientlyParked, 3) { + t.Fatalf("N+1 must park member 3; got %v", attemptN1.TransientlyParked) + } + if memberSliceContains(attemptN1.IncludedSet, 3) { + t.Fatal("member 3 must not be in N+1 IncludedSet (parked this attempt)") + } + + // Now compute attempt N+2 from a bundle where parked member 3 + // could not submit (legitimately), and members 1, 2, 4, 5 did + // submit. + attemptN1Hash := attemptN1.Hash() + bundleN1 := &TransitionMessage{ + AttemptContextHash: append([]byte{}, attemptN1Hash[:]...), + CoordinatorIDValue: 1, + Bundle: []LocalEvidenceSnapshot{ + {SenderIDValue: 1, AttemptContextHash: append([]byte{}, attemptN1Hash[:]...)}, + {SenderIDValue: 2, AttemptContextHash: append([]byte{}, attemptN1Hash[:]...)}, + {SenderIDValue: 4, AttemptContextHash: append([]byte{}, attemptN1Hash[:]...)}, + {SenderIDValue: 5, AttemptContextHash: append([]byte{}, attemptN1Hash[:]...)}, + }, + } + attemptN2, err := computeNextAttempt(attemptN1, bundleN1, f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("N+1 -> N+2: %v", err) + } + if !memberSliceContains(attemptN2.IncludedSet, 3) { + t.Fatalf( + "N+2 must reinstate member 3; got included %v", + attemptN2.IncludedSet, + ) + } + if memberSliceContains(attemptN2.TransientlyParked, 3) { + t.Fatal("N+2 must not re-park member 3") + } + if memberSliceContains(attemptN2.ExcludedSet, 3) { + t.Fatal("N+2 must not permanently exclude member 3") + } +} + +func TestNextAttempt_OriginalSignerSetPreservedAcrossTransitions(t *testing.T) { + f := newNextAttemptFixture() + f.bundleSenders = []group.MemberIndex{1, 2, 4, 5} // 3 silent + next, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + originalSize := len(f.included) + nextSize := len(next.IncludedSet) + len(next.ExcludedSet) + len(next.TransientlyParked) + if nextSize != originalSize { + t.Fatalf( + "original signer set size not preserved: %d vs %d", + nextSize, originalSize, + ) + } +} + +func TestNextAttempt_PolicyIsDeterministic(t *testing.T) { + f := newNextAttemptFixture() + f.bundleSenders = []group.MemberIndex{1, 2, 4, 5} + f.overflows[2] = map[group.MemberIndex]uint{1: 2} + f.overflows[5] = map[group.MemberIndex]uint{1: 2} + a, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("first compute: %v", err) + } + b, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("second compute: %v", err) + } + if a.Hash() != b.Hash() { + t.Fatalf("same inputs produced different next-attempt hashes") + } +} + +func TestNextAttempt_InfeasibilityWhenBelowThreshold(t *testing.T) { + f := newNextAttemptFixture() + f.threshold = 5 // Require all 5 members. + // Silently lose 2 members -> only 3 remain in IncludedSet, below + // threshold of 5. + f.bundleSenders = []group.MemberIndex{1, 2, 3} + _, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if !errors.Is(err, ErrAttemptInfeasible) { + t.Fatalf("expected ErrAttemptInfeasible, got %v", err) + } +} + +func TestNextAttempt_ThresholdZeroDisablesInfeasibilityCheck(t *testing.T) { + f := newNextAttemptFixture() + f.threshold = 0 + // All members silent; without the infeasibility check, the next + // attempt has zero included members. This is documented as a + // test seam, not a production state. + f.bundleSenders = []group.MemberIndex{} + // We need at least one entry in the bundle for TransitionMessage + // to be valid. Add a no-op snapshot from member 1 even though + // they're "silent" by the policy's view. The policy only looks + // at bundle senders that intersect prev.IncludedSet, which all + // of them do here. So instead let's leave member 1 in the + // bundle alone and silent the rest. + f.bundleSenders = []group.MemberIndex{1} + // IncludedSet would become {1}; for threshold=0 that's still + // permitted. + _, err := computeNextAttempt(f.prev(t), f.bundle(t), 0, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("expected success with threshold=0, got %v", err) + } +} + +func TestNextAttempt_OverflowFromMultipleObserversIsSummed(t *testing.T) { + f := newNextAttemptFixture() + // 2 observers each report 2 overflow events = total 4 = threshold. + f.overflows[1] = map[group.MemberIndex]uint{3: 2} + f.overflows[2] = map[group.MemberIndex]uint{3: 2} + next, err := computeNextAttempt(f.prev(t), f.bundle(t), f.threshold, f.dkgGroupPublicKey) + if err != nil { + t.Fatalf("compute: %v", err) + } + if !memberSliceContains(next.ExcludedSet, 3) { + t.Fatalf( + "sender 3 should be excluded by summed overflow; got %v", + next.ExcludedSet, + ) + } +} + +func TestNextAttempt_NilBundleRejected(t *testing.T) { + c := newSignedCoordinatorForMember(0) + handle, _ := c.BeginAttempt(newTestContext(t)) + _, err := c.NextAttempt(handle, nil, 3, []byte{0x01}) + if err == nil { + t.Fatal("expected error for nil bundle") + } +} + +func TestNextAttempt_UnknownHandleRejected(t *testing.T) { + c := newSignedCoordinatorForMember(0) + bogus := AttemptHandle{id: 999} + _, err := c.NextAttempt(bogus, &TransitionMessage{}, 3, []byte{0x01}) + if !errors.Is(err, ErrUnknownAttempt) { + t.Fatalf("expected ErrUnknownAttempt, got %v", err) + } +} + +func TestOverflowExclusionThreshold_MatchesRFC(t *testing.T) { + if OverflowExclusionThreshold != 4 { + t.Fatalf( + "RFC-21 Layer B specifies overflow threshold = 4; constant is %d", + OverflowExclusionThreshold, + ) + } +} + +func memberSliceContains(slice []group.MemberIndex, target group.MemberIndex) bool { + for _, m := range slice { + if m == target { + return true + } + } + return false +} + +func memberSlicesEqual(a, b []group.MemberIndex) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/pkg/frost/roast/seed_bridge.go b/pkg/frost/roast/seed_bridge.go new file mode 100644 index 0000000000..cfac471c59 --- /dev/null +++ b/pkg/frost/roast/seed_bridge.go @@ -0,0 +1,33 @@ +package roast + +import ( + "encoding/binary" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" +) + +// foldAttemptSeed reduces an RFC-21 [32]byte AttemptSeed to the legacy +// int64 seed accepted by SelectCoordinator. The reduction takes the +// first 8 bytes of the seed as a big-endian uint64 and re-interprets +// the bits as int64. +// +// This is a sterile, named adapter, *not* a cryptographic reduction. +// Its only contract is determinism: byte-identical input must produce +// byte-identical int64 output on every honest signer, so the +// SelectCoordinator shuffle remains in agreement across the network. +// +// The remaining 24 bytes of the seed are deliberately ignored. They +// are still part of the seed binding (so any change to those bytes is +// detected at the AttemptContext.Hash() layer, which protocol +// messages already verify in Phase 1B), but they do not influence the +// shuffle. SelectCoordinator's math.Rand source is non-cryptographic +// and 64 bits of entropy are sufficient for its purpose. +// +// Callers must not compose foldAttemptSeed with additional hashing. +// If a future RFC requires a different reduction it must be a new +// named bridge with its own tests and migration story. +func foldAttemptSeed(seed [attempt.AttemptSeedLength]byte) int64 { + // #nosec G115 -- intentional uint64-to-int64 reinterpretation; the + // downstream rand.Source accepts any int64, including negative. + return int64(binary.BigEndian.Uint64(seed[:8])) +} diff --git a/pkg/frost/roast/seed_bridge_test.go b/pkg/frost/roast/seed_bridge_test.go new file mode 100644 index 0000000000..dcc68a6c6e --- /dev/null +++ b/pkg/frost/roast/seed_bridge_test.go @@ -0,0 +1,107 @@ +package roast + +import ( + "encoding/binary" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" +) + +func TestFoldAttemptSeed_IsDeterministic(t *testing.T) { + seed := [attempt.AttemptSeedLength]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, + } + a := foldAttemptSeed(seed) + b := foldAttemptSeed(seed) + if a != b { + t.Fatalf("foldAttemptSeed not deterministic: %d != %d", a, b) + } +} + +func TestFoldAttemptSeed_TakesFirst8BytesBigEndian(t *testing.T) { + seed := [attempt.AttemptSeedLength]byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + } + got := foldAttemptSeed(seed) + if got != 1 { + t.Fatalf("first-8 BE decode wrong: got %d want 1", got) + } +} + +func TestFoldAttemptSeed_IgnoresBytesAfterIndex7(t *testing.T) { + // Document the contract: bytes 8..31 do not influence the output. + // Any change to those bytes is still caught at the + // AttemptContext.Hash() layer; the bridge merely surfaces the + // first 8. + base := [attempt.AttemptSeedLength]byte{ + 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x11, 0x22, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + mutated := base + for i := 8; i < attempt.AttemptSeedLength; i++ { + mutated[i] ^= 0xff + } + if foldAttemptSeed(base) != foldAttemptSeed(mutated) { + t.Fatal( + "bridge must ignore bytes 8..31 by contract; honest signers " + + "will desynchronise if this assumption changes", + ) + } +} + +func TestFoldAttemptSeed_FirstByteSwept(t *testing.T) { + // Sweep the high byte of the leading uint64; every value must + // produce a distinct int64. + seen := map[int64]struct{}{} + for hi := 0; hi < 256; hi++ { + var seed [attempt.AttemptSeedLength]byte + seed[0] = byte(hi) + got := foldAttemptSeed(seed) + if _, dup := seen[got]; dup { + t.Fatalf("collision on high-byte sweep at %d", hi) + } + seen[got] = struct{}{} + } + if len(seen) != 256 { + t.Fatalf("expected 256 distinct outputs, got %d", len(seen)) + } +} + +func TestFoldAttemptSeed_GoldenFixture(t *testing.T) { + // Locks the wire-format reduction so any future change to the + // bridge implementation is caught at code review. Two coordinator + // instances that disagree on this constant will produce + // divergent SelectCoordinator outputs and fracture the network. + seed := [attempt.AttemptSeedLength]byte{ + 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe, + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + } + want := int64(binary.BigEndian.Uint64(seed[:8])) + got := foldAttemptSeed(seed) + if got != want { + t.Fatalf( + "golden fixture drift: got %d want %d (seed=%x)", + got, want, seed[:8], + ) + } + // Also assert the literal integer so a typo in the reference + // computation above is caught: 0xdeadbeefcafebabe (16045690984503098046 + // as uint64) reinterpreted as int64. + const wantLiteral int64 = -2401053089206453570 + if got != wantLiteral { + t.Fatalf( + "golden fixture int64 drift: got %d want %d", + got, wantLiteral, + ) + } +} diff --git a/pkg/frost/roast/signature.go b/pkg/frost/roast/signature.go new file mode 100644 index 0000000000..7e841ee6be --- /dev/null +++ b/pkg/frost/roast/signature.go @@ -0,0 +1,257 @@ +package roast + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// Signer produces operator-key signatures over canonical-encoded +// payloads. The ROAST coordinator state machine uses one Signer per +// node to sign its own LocalEvidenceSnapshot before broadcast, and +// the elected coordinator uses the same Signer to sign the assembled +// TransitionMessage bundle. +// +// Phase 3.3 (this file) defines the interface. Phase 4 wires it to +// pkg/net's operator-key signing surface so signatures are +// automatically attributable to the node's libp2p identity. +// +// Implementations must be safe for concurrent calls from multiple +// goroutines. +type Signer interface { + // Sign returns a signature over the canonical payload produced + // by CanonicalSnapshotBytes or CanonicalBundleBytes. The + // returned signature is treated as opaque bytes by the + // coordinator state machine; the SignatureVerifier is the only + // component that interprets the byte sequence. + Sign(payload []byte) ([]byte, error) +} + +// SignatureVerifier verifies a signature attributed to a specific +// member. The verifier owns the member-to-public-key mapping; the +// coordinator state machine does not see public keys directly. +// +// Phase 3.3 (this file) defines the interface. Phase 4 wires it to +// pkg/net's member-keys table. +// +// Implementations must be safe for concurrent calls from multiple +// goroutines. +type SignatureVerifier interface { + // Verify returns nil if signature is a valid signature over + // payload produced by the operator key of signer. Returns a + // descriptive error otherwise. + Verify(payload []byte, signature []byte, signer group.MemberIndex) error +} + +// ErrSignatureInvalid is the canonical sentinel a SignatureVerifier +// returns when a signature does not validate against the supplied +// payload and signer. Callers that want to distinguish +// signature-verification failure from other errors should use +// errors.Is(err, ErrSignatureInvalid). +var ErrSignatureInvalid = errors.New("roast: signature is invalid") + +// ErrSignatureMissing is returned by VerifyBundle when a snapshot +// or bundle lacks the signature the protocol requires. +var ErrSignatureMissing = errors.New("roast: signature missing") + +// ErrCensorshipDetected is returned by VerifyBundle when a receiver +// finds its own LocalEvidenceSnapshot absent from a bundle the +// receiver expected to be present in. The receiver's snapshot is +// missing either because the elected coordinator dropped it +// (malicious or otherwise) or because the bundle was constructed +// before the receiver's submission arrived. In either case, the +// receiver must not feed the bundle into NextAttempt. +var ErrCensorshipDetected = errors.New( + "roast: own evidence snapshot missing from transition bundle (censorship or race)", +) + +// NoOpSigner returns a Signer whose Sign returns an empty signature. +// Suitable as a default in tests that do not exercise the signature +// pipeline, and as the implicit default of NewInMemoryCoordinator +// (which is preserved for backward compatibility with Phase 3.1 +// callers). +// +// A NoOpSigner-produced bundle is rejected by any non-NoOp verifier: +// the verifier sees a missing signature and fails closed. So the +// pair {NoOpSigner, NoOpSignatureVerifier} is only suitable when the +// caller wants to test the structural-aggregation pipeline in +// isolation from the crypto pipeline. +func NoOpSigner() Signer { return noOpSigner{} } + +// NoOpSignatureVerifier returns a SignatureVerifier that accepts +// every signature, including empty ones. Use ONLY in tests that do +// not exercise the signature pipeline. +func NoOpSignatureVerifier() SignatureVerifier { return noOpSignatureVerifier{} } + +type noOpSigner struct{} + +func (noOpSigner) Sign(_ []byte) ([]byte, error) { return nil, nil } + +type noOpSignatureVerifier struct{} + +func (noOpSignatureVerifier) Verify(_, _ []byte, _ group.MemberIndex) error { + return nil +} + +// CanonicalSnapshotBytes returns the byte stream over which a signer +// signs a LocalEvidenceSnapshot. The encoding excludes the +// OperatorSignature field so a verifier can recompute the bytes from +// the snapshot it received over the wire. +// +// The encoding is canonical JSON: the Overflows slice must already +// be sorted ascending by Sender (NewLocalEvidenceSnapshot guarantees +// this; Unmarshal enforces it). Any two honest signers seeing the +// same snapshot fields produce byte-identical canonical bytes. +func CanonicalSnapshotBytes(s *LocalEvidenceSnapshot) ([]byte, error) { + if s == nil { + return nil, errors.New("roast: cannot canonicalise a nil snapshot") + } + clone := LocalEvidenceSnapshot{ + SenderIDValue: s.SenderIDValue, + AttemptContextHash: s.AttemptContextHash, + Overflows: s.Overflows, + // OperatorSignature intentionally omitted -- it is the + // signature *over* this canonical encoding, not part of it. + } + return json.Marshal(&clone) +} + +// CanonicalBundleBytes returns the byte stream over which the elected +// coordinator signs a TransitionMessage. The encoding excludes the +// CoordinatorSignature field but *includes* every snapshot's +// OperatorSignature -- the coordinator's signature attests that +// these specific signed snapshots were assembled in this specific +// order. +// +// The Bundle slice must already be sorted ascending by SenderID; the +// canonical encoding assumes that invariant holds. +func CanonicalBundleBytes(m *TransitionMessage) ([]byte, error) { + if m == nil { + return nil, errors.New("roast: cannot canonicalise a nil transition message") + } + clone := TransitionMessage{ + AttemptContextHash: m.AttemptContextHash, + CoordinatorIDValue: m.CoordinatorIDValue, + Bundle: m.Bundle, + // CoordinatorSignature intentionally omitted. + } + return json.Marshal(&clone) +} + +// verifySnapshotSignature checks the OperatorSignature on a single +// LocalEvidenceSnapshot against the verifier's record of the +// snapshot's sender's operator key. +func verifySnapshotSignature( + verifier SignatureVerifier, + snapshot *LocalEvidenceSnapshot, +) error { + if len(snapshot.OperatorSignature) == 0 { + return fmt.Errorf( + "%w: snapshot from sender %d has no operator signature", + ErrSignatureMissing, + snapshot.SenderID(), + ) + } + payload, err := CanonicalSnapshotBytes(snapshot) + if err != nil { + return fmt.Errorf("canonical snapshot bytes: %w", err) + } + if err := verifier.Verify( + payload, + snapshot.OperatorSignature, + snapshot.SenderID(), + ); err != nil { + return fmt.Errorf( + "%w: sender %d: %s", + ErrSignatureInvalid, + snapshot.SenderID(), + err.Error(), + ) + } + return nil +} + +// verifyBundleSignature checks the CoordinatorSignature on a +// TransitionMessage against the verifier's record of the bundle's +// declared coordinator's operator key. The coordinator member index +// passed in must match the elected coordinator for the attempt; the +// caller (Coordinator.VerifyBundle) resolves this from the +// AttemptHandle. +func verifyBundleSignature( + verifier SignatureVerifier, + msg *TransitionMessage, + expectedCoordinator group.MemberIndex, +) error { + if len(msg.CoordinatorSignature) == 0 { + return fmt.Errorf( + "%w: transition message has no coordinator signature", + ErrSignatureMissing, + ) + } + if msg.CoordinatorID() != expectedCoordinator { + return fmt.Errorf( + "transition message coordinator id %d does not match expected %d for the attempt", + msg.CoordinatorID(), + expectedCoordinator, + ) + } + payload, err := CanonicalBundleBytes(msg) + if err != nil { + return fmt.Errorf("canonical bundle bytes: %w", err) + } + if err := verifier.Verify( + payload, + msg.CoordinatorSignature, + msg.CoordinatorID(), + ); err != nil { + return fmt.Errorf( + "%w: coordinator %d: %s", + ErrSignatureInvalid, + msg.CoordinatorID(), + err.Error(), + ) + } + return nil +} + +// verifyOwnObservationsPresent is the receiver-side censorship- +// detection check: every receiver that has already submitted its +// own LocalEvidenceSnapshot to the elected coordinator must find +// that snapshot in the resulting bundle. A coordinator that drops a +// receiver's snapshot is detected here. +// +// When selfMember is zero, the check is skipped: that signals a +// caller that has not (yet) submitted its own snapshot and therefore +// has no censorship claim to verify. +func verifyOwnObservationsPresent( + msg *TransitionMessage, + selfMember group.MemberIndex, + selfSubmission *LocalEvidenceSnapshot, +) error { + if selfMember == 0 || selfSubmission == nil { + return nil + } + for i := range msg.Bundle { + if msg.Bundle[i].SenderID() != selfMember { + continue + } + // Found the receiver's snapshot. The submitted-vs-bundled + // signature must be byte-identical -- a coordinator that + // re-signed or mutated the submission has tampered with + // observed evidence. + if !bytes.Equal( + msg.Bundle[i].OperatorSignature, + selfSubmission.OperatorSignature, + ) { + return fmt.Errorf( + "%w: own evidence snapshot signature mutated in bundle", + ErrCensorshipDetected, + ) + } + return nil + } + return ErrCensorshipDetected +} diff --git a/pkg/frost/roast/signature_test.go b/pkg/frost/roast/signature_test.go new file mode 100644 index 0000000000..1c37c53380 --- /dev/null +++ b/pkg/frost/roast/signature_test.go @@ -0,0 +1,252 @@ +package roast + +import ( + "bytes" + "crypto/sha256" + "errors" + "sync" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// fakeSigner produces deterministic signatures of the form +// SHA256(memberID || payload) so tests can exercise the sign / verify +// pipeline without real crypto. Two fakeSigners with the same member +// id produce identical signatures. +type fakeSigner struct { + id group.MemberIndex +} + +func (f *fakeSigner) Sign(payload []byte) ([]byte, error) { + h := sha256.New() + h.Write([]byte{byte(f.id)}) + h.Write(payload) + return h.Sum(nil), nil +} + +// fakeVerifier mirrors fakeSigner's deterministic signature scheme so +// every member's signatures verify against the same recomputation. +// A signature attributed to memberID is valid iff it equals +// SHA256(memberID || payload). +type fakeVerifier struct{} + +func (fakeVerifier) Verify(payload, signature []byte, signer group.MemberIndex) error { + h := sha256.New() + h.Write([]byte{byte(signer)}) + h.Write(payload) + expected := h.Sum(nil) + if !bytes.Equal(expected, signature) { + return errors.New("fakeVerifier: signature does not match recomputed value") + } + return nil +} + +func TestNoOpSigner_ReturnsEmptySignature(t *testing.T) { + sig, err := NoOpSigner().Sign([]byte("payload")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(sig) != 0 { + t.Fatalf("expected empty signature, got %x", sig) + } +} + +func TestNoOpSignatureVerifier_AcceptsEverything(t *testing.T) { + v := NoOpSignatureVerifier() + if err := v.Verify([]byte("a"), []byte("b"), 1); err != nil { + t.Fatalf("NoOp must accept everything: %v", err) + } + if err := v.Verify(nil, nil, 1); err != nil { + t.Fatalf("NoOp must accept nil payload + nil sig: %v", err) + } +} + +func TestNoOpSigner_IsConcurrencySafe(t *testing.T) { + signer := NoOpSigner() + var wg sync.WaitGroup + for i := 0; i < 32; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 32; j++ { + if _, err := signer.Sign([]byte("payload")); err != nil { + t.Errorf("Sign error under concurrency: %v", err) + return + } + } + }() + } + wg.Wait() +} + +func TestCanonicalSnapshotBytes_ExcludesOperatorSignature(t *testing.T) { + snap := NewLocalEvidenceSnapshot(7, pinnedContextHash, attempt.Evidence{ + Overflows: map[group.MemberIndex]uint{1: 2, 3: 4}, + }) + withoutSig, err := CanonicalSnapshotBytes(snap) + if err != nil { + t.Fatalf("canonical bytes (no sig): %v", err) + } + snap.OperatorSignature = []byte{0xff, 0xee} + withSig, err := CanonicalSnapshotBytes(snap) + if err != nil { + t.Fatalf("canonical bytes (with sig): %v", err) + } + if !bytes.Equal(withoutSig, withSig) { + t.Fatalf( + "adding OperatorSignature changed canonical bytes; got %s vs %s", + string(withoutSig), string(withSig), + ) + } +} + +func TestCanonicalSnapshotBytes_RejectsNil(t *testing.T) { + if _, err := CanonicalSnapshotBytes(nil); err == nil { + t.Fatal("expected error for nil snapshot") + } +} + +func TestCanonicalBundleBytes_ExcludesCoordinatorSignatureButIncludesSnapshots(t *testing.T) { + msg := buildValidTransitionMessage() + // Make sure each snapshot's OperatorSignature is non-empty so we + // can verify they appear in the canonical bytes. + for i := range msg.Bundle { + msg.Bundle[i].OperatorSignature = []byte{byte(i + 1)} + } + msg.CoordinatorSignature = []byte{0xaa, 0xbb} + canonical, err := CanonicalBundleBytes(msg) + if err != nil { + t.Fatalf("canonical bundle: %v", err) + } + // CoordinatorSignature bytes should not appear in the canonical + // payload (omitempty + nil in clone). + if bytes.Contains(canonical, []byte{0xaa, 0xbb}) { + t.Fatalf( + "CoordinatorSignature 0xaabb leaked into canonical bytes: %s", + string(canonical), + ) + } + // Each snapshot's OperatorSignature should appear via base64 + // "AQ==", "Ag==", "Aw==" (1, 2, 3 → 0x01, 0x02, 0x03). + for _, want := range []string{`"AQ=="`, `"Ag=="`, `"Aw=="`} { + if !bytes.Contains(canonical, []byte(want)) { + t.Fatalf( + "expected per-snapshot OperatorSignature %q in canonical bundle: %s", + want, string(canonical), + ) + } + } +} + +func TestCanonicalBundleBytes_RejectsNil(t *testing.T) { + if _, err := CanonicalBundleBytes(nil); err == nil { + t.Fatal("expected error for nil message") + } +} + +func TestVerifySnapshotSignature_RoundTripsThroughFakeSignerVerifier(t *testing.T) { + signer := &fakeSigner{id: 7} + snap := NewLocalEvidenceSnapshot(7, pinnedContextHash, attempt.Evidence{}) + payload, err := CanonicalSnapshotBytes(snap) + if err != nil { + t.Fatalf("canonical: %v", err) + } + sig, err := signer.Sign(payload) + if err != nil { + t.Fatalf("sign: %v", err) + } + snap.OperatorSignature = sig + if err := verifySnapshotSignature(fakeVerifier{}, snap); err != nil { + t.Fatalf("expected valid signature, got %v", err) + } +} + +func TestVerifySnapshotSignature_RejectsMissingSignature(t *testing.T) { + snap := NewLocalEvidenceSnapshot(7, pinnedContextHash, attempt.Evidence{}) + err := verifySnapshotSignature(fakeVerifier{}, snap) + if !errors.Is(err, ErrSignatureMissing) { + t.Fatalf("expected ErrSignatureMissing, got %v", err) + } +} + +func TestVerifySnapshotSignature_RejectsTamperedPayload(t *testing.T) { + signer := &fakeSigner{id: 7} + snap := NewLocalEvidenceSnapshot(7, pinnedContextHash, attempt.Evidence{}) + payload, _ := CanonicalSnapshotBytes(snap) + sig, _ := signer.Sign(payload) + snap.OperatorSignature = sig + // Tamper: change the overflow set; the recomputed canonical + // bytes will no longer match. + snap.Overflows = []OverflowEntry{{Sender: 99, Count: 1}} + if err := verifySnapshotSignature(fakeVerifier{}, snap); !errors.Is(err, ErrSignatureInvalid) { + t.Fatalf("expected ErrSignatureInvalid, got %v", err) + } +} + +func TestVerifyBundleSignature_RoundTrip(t *testing.T) { + signer := &fakeSigner{id: 11} + msg := buildValidTransitionMessage() + msg.CoordinatorIDValue = 11 + msg.CoordinatorSignature = nil + payload, _ := CanonicalBundleBytes(msg) + sig, _ := signer.Sign(payload) + msg.CoordinatorSignature = sig + if err := verifyBundleSignature(fakeVerifier{}, msg, 11); err != nil { + t.Fatalf("expected verified, got %v", err) + } +} + +func TestVerifyBundleSignature_RejectsCoordinatorMismatch(t *testing.T) { + msg := buildValidTransitionMessage() + msg.CoordinatorIDValue = 1 + msg.CoordinatorSignature = []byte{0x01} + err := verifyBundleSignature(fakeVerifier{}, msg, 99) + if err == nil { + t.Fatal("expected coordinator mismatch error") + } +} + +func TestVerifyOwnObservationsPresent_RequiresIdenticalSignature(t *testing.T) { + selfSubmission := NewLocalEvidenceSnapshot(7, pinnedContextHash, attempt.Evidence{}) + selfSubmission.OperatorSignature = []byte{0xab} + bundle := &TransitionMessage{ + Bundle: []LocalEvidenceSnapshot{ + func() LocalEvidenceSnapshot { + s := *selfSubmission + s.OperatorSignature = []byte{0xff} + return s + }(), + }, + } + if err := verifyOwnObservationsPresent(bundle, 7, selfSubmission); !errors.Is(err, ErrCensorshipDetected) { + t.Fatalf("expected ErrCensorshipDetected on mutated sig, got %v", err) + } +} + +func TestVerifyOwnObservationsPresent_DetectsMissingSnapshot(t *testing.T) { + selfSubmission := NewLocalEvidenceSnapshot(7, pinnedContextHash, attempt.Evidence{}) + bundle := &TransitionMessage{ + Bundle: []LocalEvidenceSnapshot{ + *NewLocalEvidenceSnapshot(8, pinnedContextHash, attempt.Evidence{}), + }, + } + if err := verifyOwnObservationsPresent(bundle, 7, selfSubmission); !errors.Is(err, ErrCensorshipDetected) { + t.Fatalf("expected ErrCensorshipDetected, got %v", err) + } +} + +func TestVerifyOwnObservationsPresent_SkipsWhenSelfZero(t *testing.T) { + bundle := &TransitionMessage{Bundle: []LocalEvidenceSnapshot{}} + if err := verifyOwnObservationsPresent(bundle, 0, nil); err != nil { + t.Fatalf("expected skip, got %v", err) + } +} + +func TestVerifyOwnObservationsPresent_SkipsWhenNoSelfSubmission(t *testing.T) { + bundle := &TransitionMessage{Bundle: []LocalEvidenceSnapshot{}} + if err := verifyOwnObservationsPresent(bundle, 7, nil); err != nil { + t.Fatalf("expected skip when no self submission, got %v", err) + } +} diff --git a/pkg/frost/roast/signing_retry_adapter.go b/pkg/frost/roast/signing_retry_adapter.go new file mode 100644 index 0000000000..b65a4cfd06 --- /dev/null +++ b/pkg/frost/roast/signing_retry_adapter.go @@ -0,0 +1,142 @@ +package roast + +import ( + "fmt" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// MemberToParticipantResolver maps a ROAST group.MemberIndex to the +// participant-identifier type the legacy signing-retry path uses +// (typically chain.Address in keep-core production flows, but the +// interface is intentionally generic in T so pkg/frost/roast does +// not import any caller-side type). +// +// Implementations are wallet-scoped: each FROST signing flow +// constructs a resolver from its existing wallet/group state at the +// call site and passes it to EvaluateRoastRetryForSigning or +// SigningRetryAdapter. +type MemberToParticipantResolver[T any] interface { + // For returns the participant identifier corresponding to the + // given member index. Returns an error if the member is unknown + // to the resolver (out-of-range index, evicted member, etc.). + For(member group.MemberIndex) (T, error) +} + +// EvaluateRoastRetryForSigning bridges the ROAST coordinator state +// machine with the legacy signing-retry shape. Given the previous +// attempt's handle and a verified TransitionMessage, it computes +// the next attempt's IncludedSet, converts each member index to its +// resolver-supplied participant identifier, and returns both the +// participant list and the full AttemptContext. +// +// Callers MUST call Coordinator.VerifyBundle on bundle before +// passing it to this function; the bundle is the load-bearing +// authoritative input to NextAttempt and an unverified bundle would +// silently fracture multi-instance agreement. +// +// Returns ErrAttemptInfeasible directly when the next attempt's +// included set would drop below threshold; the caller must +// propagate that to the session manager rather than swallow it. +// See RFC-21 Phase-5 Resolved Decision on infeasibility. +// +// The function is generic in T so it can be used with chain.Address +// in production keep-core flows and with simple test types +// (strings, ints) in unit tests. +func EvaluateRoastRetryForSigning[T any]( + coord Coordinator, + handle AttemptHandle, + bundle *TransitionMessage, + threshold uint, + dkgGroupPublicKey []byte, + resolver MemberToParticipantResolver[T], +) ([]T, attempt.AttemptContext, error) { + if coord == nil { + return nil, attempt.AttemptContext{}, fmt.Errorf( + "roast retry adapter: coordinator is nil", + ) + } + if resolver == nil { + var zero T + _ = zero + return nil, attempt.AttemptContext{}, fmt.Errorf( + "roast retry adapter: resolver is nil", + ) + } + nextCtx, err := coord.NextAttempt(handle, bundle, threshold, dkgGroupPublicKey) + if err != nil { + return nil, attempt.AttemptContext{}, err + } + participants := make([]T, 0, len(nextCtx.IncludedSet)) + for _, m := range nextCtx.IncludedSet { + t, err := resolver.For(m) + if err != nil { + return nil, attempt.AttemptContext{}, fmt.Errorf( + "roast retry adapter: resolver failed for member %d: %w", + m, + err, + ) + } + participants = append(participants, t) + } + return participants, nextCtx, nil +} + +// SigningRetryAdapter binds the inputs to EvaluateRoastRetryForSigning +// onto a struct so call sites can hold the configuration once and +// call EvaluateRetryParticipantsForSigning (legacy-shaped) per +// retry. Phase 6 migrates call sites to either the function or the +// struct -- whichever fits the existing call shape. +type SigningRetryAdapter[T any] struct { + Coordinator Coordinator + Handle AttemptHandle + Bundle *TransitionMessage + Threshold uint + DkgGroupPublicKey []byte + Resolver MemberToParticipantResolver[T] +} + +// EvaluateRetryParticipantsForSigning matches the shape of the +// legacy helper in pkg/frost/retry so call sites can adopt the +// adapter without changing their function-call surface. The legacy +// signature's parameters (groupMembers, seed, retryCount, +// retryParticipantsCount) are ignored: the AttemptContext bound to +// the handle is the source of truth for next-attempt selection. +// +// Returns the next IncludedSet's participants and any error from +// NextAttempt (typically ErrAttemptInfeasible). +func (a SigningRetryAdapter[T]) EvaluateRetryParticipantsForSigning( + _ []T, + _ int64, + _ uint, + _ uint, +) ([]T, error) { + participants, _, err := EvaluateRoastRetryForSigning( + a.Coordinator, + a.Handle, + a.Bundle, + a.Threshold, + a.DkgGroupPublicKey, + a.Resolver, + ) + return participants, err +} + +// NextAttemptContext returns the AttemptContext the adapter would +// transition to. Useful when callers need both the participant +// list and the context (e.g. to re-bind session orchestration to +// the new attempt's handle). +func (a SigningRetryAdapter[T]) NextAttemptContext() ( + attempt.AttemptContext, error, +) { + _, ctx, err := EvaluateRoastRetryForSigning( + a.Coordinator, + a.Handle, + a.Bundle, + a.Threshold, + a.DkgGroupPublicKey, + a.Resolver, + ) + return ctx, err +} diff --git a/pkg/frost/roast/signing_retry_adapter_test.go b/pkg/frost/roast/signing_retry_adapter_test.go new file mode 100644 index 0000000000..6272cc32c0 --- /dev/null +++ b/pkg/frost/roast/signing_retry_adapter_test.go @@ -0,0 +1,251 @@ +package roast + +import ( + "errors" + "fmt" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// addressResolverString is a deterministic resolver that maps +// member index N to the string "addr-N". Used by the adapter +// tests to verify the conversion path without needing chain types. +type addressResolverString struct{} + +func (addressResolverString) For(m group.MemberIndex) (string, error) { + if m == 0 { + return "", fmt.Errorf("zero member index") + } + return fmt.Sprintf("addr-%d", m), nil +} + +// failingResolver always errors. Used to verify that resolver +// failures propagate cleanly through the adapter. +type failingResolver struct{ err error } + +func (f failingResolver) For(_ group.MemberIndex) (string, error) { + return "", f.err +} + +// retryAdapterFixture provides a previously-completed attempt with +// a verified bundle that NextAttempt can transition from. +type retryAdapterFixture struct { + coord Coordinator + handle AttemptHandle + bundle *TransitionMessage + threshold uint + dkgPub []byte +} + +func newRetryAdapterFixture(t *testing.T) *retryAdapterFixture { + t.Helper() + members := []group.MemberIndex{1, 2, 3, 4, 5} + + // Use a throwaway coordinator to discover the elected + // coordinator, then build a real coordinator bound to that + // member as the aggregator. + scratch := NewInMemoryCoordinator() + ctx := mustBuildContext(t, members, nil, nil) + h0, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(h0) + + aggregator := newSignedCoordinatorForMember(elected) + handle, err := aggregator.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + for _, m := range members { + snap := signSnapshotForTest(t, NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{})) + if err := aggregator.RecordEvidence(handle, snap); err != nil { + t.Fatalf("record %d: %v", m, err) + } + } + bundle, err := aggregator.AggregateBundle(handle) + if err != nil { + t.Fatalf("aggregate: %v", err) + } + return &retryAdapterFixture{ + coord: aggregator, + handle: handle, + bundle: bundle, + threshold: 3, + dkgPub: []byte{0xab, 0xcd, 0xef}, + } +} + +func mustBuildContext( + t *testing.T, + included, excluded, parked []group.MemberIndex, +) attempt.AttemptContext { + t.Helper() + ctx, err := attempt.NewAttemptContextWithParking( + "session-test", + "key-group-test", + []byte{0xab, 0xcd, 0xef}, + [attempt.MessageDigestLength]byte{0x42}, + 0, + included, + excluded, + parked, + ) + if err != nil { + t.Fatalf("build ctx: %v", err) + } + return ctx +} + +func TestEvaluateRoastRetryForSigning_HappyPath(t *testing.T) { + f := newRetryAdapterFixture(t) + + addresses, nextCtx, err := EvaluateRoastRetryForSigning[string]( + f.coord, f.handle, f.bundle, f.threshold, f.dkgPub, + addressResolverString{}, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(addresses) != 5 { + t.Fatalf("expected 5 addresses, got %d", len(addresses)) + } + for i, a := range addresses { + want := fmt.Sprintf("addr-%d", nextCtx.IncludedSet[i]) + if a != want { + t.Fatalf( + "address[%d]: got %q want %q", + i, a, want, + ) + } + } + if nextCtx.AttemptNumber != 1 { + t.Fatalf("attempt number: got %d want 1", nextCtx.AttemptNumber) + } +} + +func TestEvaluateRoastRetryForSigning_PropagatesInfeasibility(t *testing.T) { + f := newRetryAdapterFixture(t) + + _, _, err := EvaluateRoastRetryForSigning[string]( + f.coord, f.handle, f.bundle, 99, f.dkgPub, + addressResolverString{}, + ) + if !errors.Is(err, ErrAttemptInfeasible) { + t.Fatalf("expected ErrAttemptInfeasible, got %v", err) + } +} + +func TestEvaluateRoastRetryForSigning_PropagatesResolverError(t *testing.T) { + f := newRetryAdapterFixture(t) + + sentinel := errors.New("resolver lookup failed") + _, _, err := EvaluateRoastRetryForSigning[string]( + f.coord, f.handle, f.bundle, f.threshold, f.dkgPub, + failingResolver{err: sentinel}, + ) + if err == nil { + t.Fatal("expected resolver error") + } + if !errors.Is(err, sentinel) { + t.Fatalf("expected wrapped sentinel, got %v", err) + } +} + +func TestEvaluateRoastRetryForSigning_RejectsNilCoordinator(t *testing.T) { + _, _, err := EvaluateRoastRetryForSigning[string]( + nil, AttemptHandle{}, &TransitionMessage{}, 3, []byte{0x01}, + addressResolverString{}, + ) + if err == nil { + t.Fatal("expected nil-coordinator error") + } +} + +func TestEvaluateRoastRetryForSigning_RejectsNilResolver(t *testing.T) { + _, _, err := EvaluateRoastRetryForSigning[string]( + NewInMemoryCoordinator(), + AttemptHandle{}, &TransitionMessage{}, 3, []byte{0x01}, + nil, + ) + if err == nil { + t.Fatal("expected nil-resolver error") + } +} + +func TestSigningRetryAdapter_LegacyShapeMatchesPureFunction(t *testing.T) { + f := newRetryAdapterFixture(t) + resolver := addressResolverString{} + + adapter := SigningRetryAdapter[string]{ + Coordinator: f.coord, + Handle: f.handle, + Bundle: f.bundle, + Threshold: f.threshold, + DkgGroupPublicKey: f.dkgPub, + Resolver: resolver, + } + + // Legacy parameters are ignored. + viaAdapter, err := adapter.EvaluateRetryParticipantsForSigning( + nil, 0, 0, 0, + ) + if err != nil { + t.Fatalf("adapter: %v", err) + } + viaFunc, _, err := EvaluateRoastRetryForSigning[string]( + f.coord, f.handle, f.bundle, f.threshold, f.dkgPub, resolver, + ) + if err != nil { + t.Fatalf("function: %v", err) + } + if len(viaAdapter) != len(viaFunc) { + t.Fatalf( + "adapter and function disagree on participant count: %d vs %d", + len(viaAdapter), len(viaFunc), + ) + } + for i := range viaAdapter { + if viaAdapter[i] != viaFunc[i] { + t.Fatalf("adapter[%d] = %q, function[%d] = %q", i, viaAdapter[i], i, viaFunc[i]) + } + } +} + +func TestSigningRetryAdapter_NextAttemptContextRoundTrip(t *testing.T) { + f := newRetryAdapterFixture(t) + adapter := SigningRetryAdapter[string]{ + Coordinator: f.coord, + Handle: f.handle, + Bundle: f.bundle, + Threshold: f.threshold, + DkgGroupPublicKey: f.dkgPub, + Resolver: addressResolverString{}, + } + ctx1, err := adapter.NextAttemptContext() + if err != nil { + t.Fatalf("first: %v", err) + } + ctx2, err := adapter.NextAttemptContext() + if err != nil { + t.Fatalf("second: %v", err) + } + if ctx1.Hash() != ctx2.Hash() { + t.Fatal("NextAttemptContext must be deterministic across calls") + } +} + +func TestSigningRetryAdapter_PropagatesInfeasibility(t *testing.T) { + f := newRetryAdapterFixture(t) + adapter := SigningRetryAdapter[string]{ + Coordinator: f.coord, + Handle: f.handle, + Bundle: f.bundle, + Threshold: 99, + DkgGroupPublicKey: f.dkgPub, + Resolver: addressResolverString{}, + } + _, err := adapter.EvaluateRetryParticipantsForSigning(nil, 0, 0, 0) + if !errors.Is(err, ErrAttemptInfeasible) { + t.Fatalf("expected ErrAttemptInfeasible, got %v", err) + } +} diff --git a/pkg/frost/roast/transition_message.go b/pkg/frost/roast/transition_message.go new file mode 100644 index 0000000000..f8747bd4b7 --- /dev/null +++ b/pkg/frost/roast/transition_message.go @@ -0,0 +1,405 @@ +package roast + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "sort" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// roastMessageTypePrefix is the per-protocol prefix every ROAST-layer +// wire message uses for its net.TaggedUnmarshaler Type(). Distinct +// from frost_signing/native_frost/ and frost_signing/native_tbtc_signer/ +// so the network router can dispatch unambiguously. +const roastMessageTypePrefix = "frost_signing/roast/" + +// LocalEvidenceSnapshotType is the stable Type() string for a single +// signer's signed evidence snapshot. +const LocalEvidenceSnapshotType = roastMessageTypePrefix + "local_evidence_snapshot" + +// TransitionMessageType is the stable Type() string for the +// coordinator-aggregated bundle. +const TransitionMessageType = roastMessageTypePrefix + "transition_message" + +// MaxSnapshotsPerBundle caps the number of LocalEvidenceSnapshot +// entries a TransitionMessage may carry. Sized for the worst-case +// production signing group plus headroom; rejects pathological +// bundles at Unmarshal time so a misbehaving peer cannot exhaust +// memory on the receiver. +const MaxSnapshotsPerBundle = 256 + +// MaxOperatorSignatureBytes caps the per-snapshot OperatorSignature +// length. Sized to accept secp256k1 DER (~72 bytes), ed25519 (64 +// bytes), and reasonable post-quantum candidates without committing +// to a specific scheme at this layer. Rejects oversize payloads. +const MaxOperatorSignatureBytes = 256 + +// MaxCoordinatorSignatureBytes caps the bundle-level +// CoordinatorSignature. Same justification as +// MaxOperatorSignatureBytes. +const MaxCoordinatorSignatureBytes = 256 + +// OverflowEntry is the JSON-friendly key/value pair representing one +// per-sender overflow count from an attempt.Evidence map. The slice +// representation is canonical (sorted by Sender ascending) so any +// two honest signers serialising the same evidence produce +// byte-identical JSON. +type OverflowEntry struct { + Sender group.MemberIndex `json:"sender"` + Count uint `json:"count"` +} + +// RejectEntry carries one per-(sender, reason) reject count from an +// attempt.Evidence map. The bundle's Rejects field is sorted +// ascending first by Sender, then by Reason, so two honest signers +// produce byte-identical canonical encodings. +type RejectEntry struct { + Sender group.MemberIndex `json:"sender"` + Reason string `json:"reason"` + Count uint `json:"count"` +} + +// ConflictEntry carries one per-sender conflict count -- the number +// of first-write-wins disagreements detected during the attempt. +// Sorted ascending by Sender for canonical encoding. +type ConflictEntry struct { + Sender group.MemberIndex `json:"sender"` + Count uint `json:"count"` +} + +// LocalEvidenceSnapshot is the per-signer signed evidence produced +// during a single attempt. It is the input to the coordinator's +// aggregation and to the receiver-side bundle verification. +// +// Phase 3.2 (this file) defines the wire type only. Signature +// computation and verification land in Phase 3.3. +type LocalEvidenceSnapshot struct { + SenderIDValue uint32 `json:"senderID"` + // AttemptContextHash binds the snapshot to the attempt the + // evidence describes. Always exactly 32 bytes. + AttemptContextHash []byte `json:"attemptContextHash"` + // Overflows is the canonical sorted form of the + // attempt.Evidence.Overflows map; sorted ascending by Sender. + // Omitted when no overflow events were observed. + Overflows []OverflowEntry `json:"overflows,omitempty"` + // Rejects is the canonical sorted form of the + // attempt.Evidence.Rejects map; sorted ascending first by Sender, + // then by Reason. Omitted when no validation-reject events were + // observed. Each entry counts the number of rejects observed + // for one (sender, reason) pair, saturated at the recorder's + // reject quota. + Rejects []RejectEntry `json:"rejects,omitempty"` + // Conflicts is the canonical sorted form of the + // attempt.Evidence.Conflicts map; sorted ascending by Sender. + // Omitted when no first-write-wins-conflict events were + // observed. + Conflicts []ConflictEntry `json:"conflicts,omitempty"` + // OperatorSignature is the signer's operator-key signature over + // the canonical encoding of (senderID, attemptContextHash, + // overflows, rejects, conflicts). Phase 3.3 defines the + // canonical-encoding algorithm and the verification routine. + OperatorSignature []byte `json:"operatorSignature,omitempty"` +} + +// NewLocalEvidenceSnapshot converts an attempt.Evidence map into a +// LocalEvidenceSnapshot ready for signing and broadcast. The +// resulting snapshot's Overflows field is sorted ascending by +// Sender for deterministic JSON encoding. The OperatorSignature is +// left empty -- the caller must sign and populate it (Phase 3.3). +func NewLocalEvidenceSnapshot( + sender group.MemberIndex, + attemptContextHash [attempt.MessageDigestLength]byte, + evidence attempt.Evidence, +) *LocalEvidenceSnapshot { + overflows := make([]OverflowEntry, 0, len(evidence.Overflows)) + for s, c := range evidence.Overflows { + overflows = append(overflows, OverflowEntry{Sender: s, Count: c}) + } + sort.Slice(overflows, func(i, j int) bool { + return overflows[i].Sender < overflows[j].Sender + }) + + rejects := make([]RejectEntry, 0) + for s, entries := range evidence.Rejects { + for _, e := range entries { + rejects = append(rejects, RejectEntry{ + Sender: s, + Reason: e.Reason, + Count: e.Count, + }) + } + } + sort.Slice(rejects, func(i, j int) bool { + if rejects[i].Sender != rejects[j].Sender { + return rejects[i].Sender < rejects[j].Sender + } + return rejects[i].Reason < rejects[j].Reason + }) + + conflicts := make([]ConflictEntry, 0, len(evidence.Conflicts)) + for s, c := range evidence.Conflicts { + conflicts = append(conflicts, ConflictEntry{Sender: s, Count: c}) + } + sort.Slice(conflicts, func(i, j int) bool { + return conflicts[i].Sender < conflicts[j].Sender + }) + + snap := &LocalEvidenceSnapshot{ + SenderIDValue: uint32(sender), + AttemptContextHash: append([]byte{}, attemptContextHash[:]...), + Overflows: overflows, + } + if len(rejects) > 0 { + snap.Rejects = rejects + } + if len(conflicts) > 0 { + snap.Conflicts = conflicts + } + return snap +} + +// SenderID returns the snapshot's sender as a group.MemberIndex. +func (s *LocalEvidenceSnapshot) SenderID() group.MemberIndex { + return group.MemberIndex(s.SenderIDValue) +} + +// AttemptContextHashArray returns the 32-byte attempt context hash +// as a fixed-size array. Returns the zero array if the field is +// malformed (caller should have validated via Unmarshal first). +func (s *LocalEvidenceSnapshot) AttemptContextHashArray() [attempt.MessageDigestLength]byte { + var out [attempt.MessageDigestLength]byte + if len(s.AttemptContextHash) == attempt.MessageDigestLength { + copy(out[:], s.AttemptContextHash) + } + return out +} + +// Evidence reconstructs the attempt.Evidence map form from the +// canonical sorted-slice representation. The returned Evidence +// shares no state with the snapshot. +func (s *LocalEvidenceSnapshot) Evidence() attempt.Evidence { + out := attempt.Evidence{ + Overflows: make(map[group.MemberIndex]uint, len(s.Overflows)), + Rejects: make(map[group.MemberIndex][]attempt.RejectEntry, 0), + Conflicts: make(map[group.MemberIndex]uint, len(s.Conflicts)), + } + for _, e := range s.Overflows { + out.Overflows[e.Sender] = e.Count + } + for _, e := range s.Rejects { + out.Rejects[e.Sender] = append(out.Rejects[e.Sender], attempt.RejectEntry{ + Reason: e.Reason, + Count: e.Count, + }) + } + for _, e := range s.Conflicts { + out.Conflicts[e.Sender] = e.Count + } + return out +} + +// Type implements net.TaggedUnmarshaler. +func (s *LocalEvidenceSnapshot) Type() string { + return LocalEvidenceSnapshotType +} + +// Marshal serialises the snapshot to canonical JSON. The Overflows +// slice is sorted by Sender ascending in NewLocalEvidenceSnapshot +// so two honest signers with the same evidence produce +// byte-identical bytes. +func (s *LocalEvidenceSnapshot) Marshal() ([]byte, error) { + return json.Marshal(s) +} + +// Unmarshal parses canonical JSON into the snapshot and validates +// the resulting structure. +func (s *LocalEvidenceSnapshot) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, s); err != nil { + return err + } + return s.Validate() +} + +// Validate runs the structural checks Unmarshal applies after a JSON +// decode. Exposed publicly so callers that construct snapshots in +// memory (e.g. the Coordinator state machine) can validate without +// a marshal/unmarshal round-trip. +func (s *LocalEvidenceSnapshot) Validate() error { + if s.SenderIDValue == 0 { + return errors.New("local evidence snapshot: senderID is zero") + } + if len(s.AttemptContextHash) != attempt.MessageDigestLength { + return fmt.Errorf( + "local evidence snapshot: attemptContextHash length [%d], expected [%d]", + len(s.AttemptContextHash), + attempt.MessageDigestLength, + ) + } + if len(s.OperatorSignature) > MaxOperatorSignatureBytes { + return fmt.Errorf( + "local evidence snapshot: operatorSignature length [%d] exceeds cap [%d]", + len(s.OperatorSignature), + MaxOperatorSignatureBytes, + ) + } + for i := 1; i < len(s.Overflows); i++ { + if s.Overflows[i].Sender <= s.Overflows[i-1].Sender { + return fmt.Errorf( + "local evidence snapshot: overflows not sorted ascending or contain duplicate at index %d", + i, + ) + } + } + for i := 1; i < len(s.Rejects); i++ { + prev := s.Rejects[i-1] + cur := s.Rejects[i] + if cur.Sender < prev.Sender { + return fmt.Errorf( + "local evidence snapshot: rejects not sorted ascending by sender at index %d", + i, + ) + } + if cur.Sender == prev.Sender && cur.Reason <= prev.Reason { + return fmt.Errorf( + "local evidence snapshot: rejects not sorted ascending by reason or contain duplicate at index %d", + i, + ) + } + } + for i := 1; i < len(s.Conflicts); i++ { + if s.Conflicts[i].Sender <= s.Conflicts[i-1].Sender { + return fmt.Errorf( + "local evidence snapshot: conflicts not sorted ascending or contain duplicate at index %d", + i, + ) + } + } + return nil +} + +// TransitionMessage is the coordinator-aggregated bundle that drives +// the deterministic NextAttempt transition. It contains every +// participating signer's signed evidence snapshot for one attempt, +// plus the coordinator's own signature over the canonical bundle. +// +// Phase 3.2 (this file) defines the wire type. Aggregation, +// canonical encoding, and verification land in Phase 3.3. +type TransitionMessage struct { + // AttemptContextHash identifies the attempt the bundle + // describes. Must match every snapshot's AttemptContextHash. + // Always exactly 32 bytes. + AttemptContextHash []byte `json:"attemptContextHash"` + // CoordinatorIDValue is the member index of the elected + // coordinator that produced this bundle. + CoordinatorIDValue uint32 `json:"coordinatorID"` + // Bundle is the canonical sorted-by-SenderID list of signed + // evidence snapshots aggregated by the coordinator. + Bundle []LocalEvidenceSnapshot `json:"bundle"` + // CoordinatorSignature is the coordinator's operator-key + // signature over the canonical encoding of the bundle. Phase + // 3.3 defines the canonical-encoding algorithm and the + // verification routine. Phase 3.2 treats this field as opaque + // bytes with a length cap. + CoordinatorSignature []byte `json:"coordinatorSignature,omitempty"` +} + +// CoordinatorID returns the coordinator member index as a +// group.MemberIndex. +func (m *TransitionMessage) CoordinatorID() group.MemberIndex { + return group.MemberIndex(m.CoordinatorIDValue) +} + +// AttemptContextHashArray returns the 32-byte attempt context hash +// as a fixed-size array. Returns the zero array if the field is +// malformed (caller should have validated via Unmarshal first). +func (m *TransitionMessage) AttemptContextHashArray() [attempt.MessageDigestLength]byte { + var out [attempt.MessageDigestLength]byte + if len(m.AttemptContextHash) == attempt.MessageDigestLength { + copy(out[:], m.AttemptContextHash) + } + return out +} + +// Type implements net.TaggedUnmarshaler. +func (m *TransitionMessage) Type() string { + return TransitionMessageType +} + +// Marshal serialises the message to canonical JSON. +func (m *TransitionMessage) Marshal() ([]byte, error) { + return json.Marshal(m) +} + +// Unmarshal parses canonical JSON into the message and validates +// the structure: hash length, bundle size cap, signature size cap, +// snapshot validity, bundle ordering by SenderID ascending, and +// every snapshot binding to the same AttemptContextHash as the +// bundle. +func (m *TransitionMessage) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, m); err != nil { + return err + } + return m.Validate() +} + +// Validate runs the structural checks Unmarshal applies after a JSON +// decode: bundle hash length, bundle size cap, coordinator id, every +// snapshot's validity, bundle ordering, and intra-bundle hash +// consistency. Exposed publicly so callers that construct messages +// in memory can validate without a marshal/unmarshal round-trip. +func (m *TransitionMessage) Validate() error { + if len(m.AttemptContextHash) != attempt.MessageDigestLength { + return fmt.Errorf( + "transition message: attemptContextHash length [%d], expected [%d]", + len(m.AttemptContextHash), + attempt.MessageDigestLength, + ) + } + if m.CoordinatorIDValue == 0 { + return errors.New("transition message: coordinatorID is zero") + } + if len(m.Bundle) == 0 { + return errors.New("transition message: bundle must not be empty") + } + if len(m.Bundle) > MaxSnapshotsPerBundle { + return fmt.Errorf( + "transition message: bundle length [%d] exceeds cap [%d]", + len(m.Bundle), + MaxSnapshotsPerBundle, + ) + } + if len(m.CoordinatorSignature) > MaxCoordinatorSignatureBytes { + return fmt.Errorf( + "transition message: coordinatorSignature length [%d] exceeds cap [%d]", + len(m.CoordinatorSignature), + MaxCoordinatorSignatureBytes, + ) + } + for i := range m.Bundle { + if err := m.Bundle[i].Validate(); err != nil { + return fmt.Errorf( + "transition message: bundle[%d] invalid: %w", + i, err, + ) + } + if !bytes.Equal(m.Bundle[i].AttemptContextHash, m.AttemptContextHash) { + return fmt.Errorf( + "transition message: bundle[%d] attempt context hash does not match bundle hash", + i, + ) + } + if i > 0 { + if m.Bundle[i].SenderIDValue <= m.Bundle[i-1].SenderIDValue { + return fmt.Errorf( + "transition message: bundle not sorted ascending by senderID or contains duplicate at index %d", + i, + ) + } + } + } + return nil +} diff --git a/pkg/frost/roast/transition_message_test.go b/pkg/frost/roast/transition_message_test.go new file mode 100644 index 0000000000..4fadf13871 --- /dev/null +++ b/pkg/frost/roast/transition_message_test.go @@ -0,0 +1,381 @@ +package roast + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +var pinnedContextHash = [attempt.MessageDigestLength]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, +} + +func TestLocalEvidenceSnapshot_TypeIsStable(t *testing.T) { + s := &LocalEvidenceSnapshot{} + if got := s.Type(); got != LocalEvidenceSnapshotType { + t.Fatalf("Type() = %q, want %q", got, LocalEvidenceSnapshotType) + } + if !strings.HasPrefix(LocalEvidenceSnapshotType, roastMessageTypePrefix) { + t.Fatalf( + "Type() must be under the %q prefix; got %q", + roastMessageTypePrefix, LocalEvidenceSnapshotType, + ) + } +} + +func TestNewLocalEvidenceSnapshot_SortsOverflows(t *testing.T) { + evidence := attempt.Evidence{ + Overflows: map[group.MemberIndex]uint{ + 5: 3, + 1: 2, + 3: 1, + }, + } + s := NewLocalEvidenceSnapshot(7, pinnedContextHash, evidence) + + if len(s.Overflows) != 3 { + t.Fatalf("expected 3 overflow entries, got %d", len(s.Overflows)) + } + for i := 1; i < len(s.Overflows); i++ { + if s.Overflows[i].Sender <= s.Overflows[i-1].Sender { + t.Fatalf( + "overflows not sorted ascending at index %d: %v", + i, s.Overflows, + ) + } + } + if s.SenderIDValue != 7 { + t.Fatalf("SenderIDValue = %d, want 7", s.SenderIDValue) + } + if !bytes.Equal(s.AttemptContextHash, pinnedContextHash[:]) { + t.Fatalf( + "AttemptContextHash mismatch: got %x want %x", + s.AttemptContextHash, pinnedContextHash[:], + ) + } +} + +func TestNewLocalEvidenceSnapshot_EmptyEvidenceOmitsOverflows(t *testing.T) { + s := NewLocalEvidenceSnapshot(1, pinnedContextHash, attempt.Evidence{ + Overflows: map[group.MemberIndex]uint{}, + }) + if len(s.Overflows) != 0 { + t.Fatalf("expected empty overflows, got %v", s.Overflows) + } + data, err := s.Marshal() + if err != nil { + t.Fatalf("marshal: %v", err) + } + if strings.Contains(string(data), "overflows") { + t.Fatalf( + "empty overflows should be omitted by omitempty; got JSON: %s", + string(data), + ) + } +} + +func TestLocalEvidenceSnapshot_RoundTrip(t *testing.T) { + original := NewLocalEvidenceSnapshot(7, pinnedContextHash, attempt.Evidence{ + Overflows: map[group.MemberIndex]uint{ + 1: 2, + 3: 1, + 5: 3, + }, + }) + original.OperatorSignature = bytes.Repeat([]byte{0xab}, 64) + + data, err := original.Marshal() + if err != nil { + t.Fatalf("marshal: %v", err) + } + decoded := &LocalEvidenceSnapshot{} + if err := decoded.Unmarshal(data); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.SenderIDValue != original.SenderIDValue { + t.Fatalf("sender mismatch") + } + if !bytes.Equal(decoded.AttemptContextHash, original.AttemptContextHash) { + t.Fatalf("attempt context hash mismatch") + } + if len(decoded.Overflows) != len(original.Overflows) { + t.Fatalf( + "overflow length mismatch: %d vs %d", + len(decoded.Overflows), len(original.Overflows), + ) + } + if !bytes.Equal(decoded.OperatorSignature, original.OperatorSignature) { + t.Fatalf("signature mismatch") + } +} + +func TestLocalEvidenceSnapshot_RejectsZeroSender(t *testing.T) { + s := &LocalEvidenceSnapshot{ + SenderIDValue: 0, + AttemptContextHash: pinnedContextHash[:], + } + data, _ := json.Marshal(s) + err := (&LocalEvidenceSnapshot{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "senderID is zero") { + t.Fatalf("expected zero-sender error, got %v", err) + } +} + +func TestLocalEvidenceSnapshot_RejectsWrongHashLength(t *testing.T) { + bad := []byte(`{ + "senderID": 1, + "attemptContextHash": "AAEC" + }`) + err := (&LocalEvidenceSnapshot{}).Unmarshal(bad) + if err == nil || !strings.Contains(err.Error(), "attemptContextHash length") { + t.Fatalf("expected hash-length error, got %v", err) + } +} + +func TestLocalEvidenceSnapshot_RejectsOversizeSignature(t *testing.T) { + s := NewLocalEvidenceSnapshot(1, pinnedContextHash, attempt.Evidence{}) + s.OperatorSignature = bytes.Repeat([]byte{0xff}, MaxOperatorSignatureBytes+1) + data, _ := json.Marshal(s) + err := (&LocalEvidenceSnapshot{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "exceeds cap") { + t.Fatalf("expected signature-cap error, got %v", err) + } +} + +func TestLocalEvidenceSnapshot_RejectsUnsortedOverflows(t *testing.T) { + bad := &LocalEvidenceSnapshot{ + SenderIDValue: 1, + AttemptContextHash: pinnedContextHash[:], + Overflows: []OverflowEntry{ + {Sender: 5, Count: 1}, + {Sender: 1, Count: 1}, + }, + } + data, _ := json.Marshal(bad) + err := (&LocalEvidenceSnapshot{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "not sorted") { + t.Fatalf("expected sort error, got %v", err) + } +} + +func TestLocalEvidenceSnapshot_RejectsDuplicateOverflowSender(t *testing.T) { + bad := &LocalEvidenceSnapshot{ + SenderIDValue: 1, + AttemptContextHash: pinnedContextHash[:], + Overflows: []OverflowEntry{ + {Sender: 3, Count: 1}, + {Sender: 3, Count: 1}, + }, + } + data, _ := json.Marshal(bad) + err := (&LocalEvidenceSnapshot{}).Unmarshal(data) + if err == nil { + t.Fatal("expected duplicate-sender error") + } +} + +func TestLocalEvidenceSnapshot_EvidenceReconstructsMap(t *testing.T) { + original := attempt.Evidence{ + Overflows: map[group.MemberIndex]uint{1: 2, 3: 4}, + } + s := NewLocalEvidenceSnapshot(7, pinnedContextHash, original) + got := s.Evidence() + if len(got.Overflows) != len(original.Overflows) { + t.Fatalf( + "map size mismatch: got %d want %d", + len(got.Overflows), len(original.Overflows), + ) + } + for k, v := range original.Overflows { + if got.Overflows[k] != v { + t.Fatalf("overflow[%d]: got %d want %d", k, got.Overflows[k], v) + } + } +} + +func TestLocalEvidenceSnapshot_AttemptContextHashArrayHandlesMalformed(t *testing.T) { + s := &LocalEvidenceSnapshot{AttemptContextHash: []byte{0x01, 0x02}} + arr := s.AttemptContextHashArray() + var zero [attempt.MessageDigestLength]byte + if arr != zero { + t.Fatalf("expected zero array for malformed hash, got %x", arr) + } +} + +func TestTransitionMessage_TypeIsStable(t *testing.T) { + m := &TransitionMessage{} + if got := m.Type(); got != TransitionMessageType { + t.Fatalf("Type() = %q, want %q", got, TransitionMessageType) + } + if !strings.HasPrefix(TransitionMessageType, roastMessageTypePrefix) { + t.Fatalf("type prefix mismatch: %q", TransitionMessageType) + } +} + +func TestTransitionMessage_RoundTrip(t *testing.T) { + m := buildValidTransitionMessage() + data, err := m.Marshal() + if err != nil { + t.Fatalf("marshal: %v", err) + } + decoded := &TransitionMessage{} + if err := decoded.Unmarshal(data); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.CoordinatorIDValue != m.CoordinatorIDValue { + t.Fatalf("coordinator id mismatch") + } + if len(decoded.Bundle) != len(m.Bundle) { + t.Fatalf( + "bundle size mismatch: %d vs %d", + len(decoded.Bundle), len(m.Bundle), + ) + } + for i := range decoded.Bundle { + if decoded.Bundle[i].SenderIDValue != m.Bundle[i].SenderIDValue { + t.Fatalf("bundle[%d] sender mismatch", i) + } + } +} + +func TestTransitionMessage_RejectsBadBundleOrdering(t *testing.T) { + m := buildValidTransitionMessage() + // Swap order to make it unsorted. + m.Bundle[0], m.Bundle[1] = m.Bundle[1], m.Bundle[0] + data, _ := json.Marshal(m) + err := (&TransitionMessage{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "not sorted") { + t.Fatalf("expected sort error, got %v", err) + } +} + +func TestTransitionMessage_RejectsMismatchedBundleHash(t *testing.T) { + m := buildValidTransitionMessage() + // Mutate the first bundled snapshot's hash so it disagrees + // with the bundle-level hash. + m.Bundle[0].AttemptContextHash = make([]byte, attempt.MessageDigestLength) + for i := range m.Bundle[0].AttemptContextHash { + m.Bundle[0].AttemptContextHash[i] = 0xff + } + data, _ := json.Marshal(m) + err := (&TransitionMessage{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "does not match bundle hash") { + t.Fatalf("expected hash-mismatch error, got %v", err) + } +} + +func TestTransitionMessage_RejectsEmptyBundle(t *testing.T) { + m := buildValidTransitionMessage() + m.Bundle = nil + data, _ := json.Marshal(m) + err := (&TransitionMessage{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "must not be empty") { + t.Fatalf("expected empty-bundle error, got %v", err) + } +} + +func TestTransitionMessage_RejectsOversizeBundle(t *testing.T) { + m := buildValidTransitionMessage() + // Grow bundle beyond the cap by duplicating with monotonically + // increasing senders. + m.Bundle = make([]LocalEvidenceSnapshot, MaxSnapshotsPerBundle+1) + for i := range m.Bundle { + m.Bundle[i] = LocalEvidenceSnapshot{ + SenderIDValue: uint32(i + 1), + AttemptContextHash: append([]byte{}, m.AttemptContextHash...), + } + } + data, _ := json.Marshal(m) + err := (&TransitionMessage{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "exceeds cap") { + t.Fatalf("expected oversize-bundle error, got %v", err) + } +} + +func TestTransitionMessage_RejectsZeroCoordinatorID(t *testing.T) { + m := buildValidTransitionMessage() + m.CoordinatorIDValue = 0 + data, _ := json.Marshal(m) + err := (&TransitionMessage{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "coordinatorID is zero") { + t.Fatalf("expected zero-coordinator error, got %v", err) + } +} + +func TestTransitionMessage_RejectsOversizeCoordinatorSignature(t *testing.T) { + m := buildValidTransitionMessage() + m.CoordinatorSignature = bytes.Repeat([]byte{0xff}, MaxCoordinatorSignatureBytes+1) + data, _ := json.Marshal(m) + err := (&TransitionMessage{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "exceeds cap") { + t.Fatalf("expected oversize-signature error, got %v", err) + } +} + +func TestTransitionMessage_RejectsBundleWithInvalidSnapshot(t *testing.T) { + m := buildValidTransitionMessage() + m.Bundle[0].SenderIDValue = 0 + data, _ := json.Marshal(m) + err := (&TransitionMessage{}).Unmarshal(data) + if err == nil || !strings.Contains(err.Error(), "senderID is zero") { + t.Fatalf("expected invalid-snapshot error, got %v", err) + } +} + +func TestTransitionMessage_RejectsDuplicateBundleSender(t *testing.T) { + m := buildValidTransitionMessage() + m.Bundle[1].SenderIDValue = m.Bundle[0].SenderIDValue + data, _ := json.Marshal(m) + err := (&TransitionMessage{}).Unmarshal(data) + if err == nil { + t.Fatal("expected duplicate-sender error") + } +} + +func TestTransitionMessage_DeterministicJSONForIdenticalInputs(t *testing.T) { + a := buildValidTransitionMessage() + b := buildValidTransitionMessage() + dataA, err := a.Marshal() + if err != nil { + t.Fatalf("marshal a: %v", err) + } + dataB, err := b.Marshal() + if err != nil { + t.Fatalf("marshal b: %v", err) + } + if !bytes.Equal(dataA, dataB) { + t.Fatalf( + "identical inputs produced different JSON:\n a=%s\n b=%s", + string(dataA), string(dataB), + ) + } +} + +func buildValidTransitionMessage() *TransitionMessage { + mkSnap := func(sender group.MemberIndex) LocalEvidenceSnapshot { + return LocalEvidenceSnapshot{ + SenderIDValue: uint32(sender), + AttemptContextHash: append([]byte{}, pinnedContextHash[:]...), + Overflows: []OverflowEntry{ + {Sender: 99, Count: 1}, + }, + } + } + return &TransitionMessage{ + AttemptContextHash: append([]byte{}, pinnedContextHash[:]...), + CoordinatorIDValue: 1, + Bundle: []LocalEvidenceSnapshot{ + mkSnap(1), + mkSnap(2), + mkSnap(3), + }, + CoordinatorSignature: bytes.Repeat([]byte{0xee}, 64), + } +} 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_context_binding.go b/pkg/frost/signing/attempt_context_binding.go new file mode 100644 index 0000000000..5bb01b2cb9 --- /dev/null +++ b/pkg/frost/signing/attempt_context_binding.go @@ -0,0 +1,72 @@ +//go:build frost_native + +package signing + +import ( + "fmt" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" +) + +// AttemptContextHashFieldLength is the on-wire byte length of the +// optional AttemptContextHash field carried by the FROST/tbtc-signer +// protocol messages. The field is the canonical SHA-256 hash of the +// AttemptContext (see pkg/frost/roast/attempt), so 32 bytes. +const AttemptContextHashFieldLength = attempt.MessageDigestLength + +// validateAttemptContextHashField checks the length invariant for the +// optional AttemptContextHash field on protocol messages. An absent +// field (nil or zero-length slice) is valid; a present field must +// match AttemptContextHashFieldLength exactly. +// +// This is the only validation Phase 1B performs on the field. Higher- +// level acceptance (the receiver-side check that the hash matches the +// locally-computed AttemptContext) lands in a later RFC-21 phase +// behind a build tag, since enabling it requires honest peers to have +// rolled out the new field first. +func validateAttemptContextHashField(field []byte) error { + if len(field) == 0 { + return nil + } + if len(field) != AttemptContextHashFieldLength { + return fmt.Errorf( + "attempt context hash field has wrong length [%d], expected [%d] or absent", + len(field), + AttemptContextHashFieldLength, + ) + } + return nil +} + +// attemptContextHashFieldFromArray converts a fixed-size 32-byte hash +// into the slice form used on the wire. Returns a fresh slice so the +// caller's array cannot be mutated through the returned reference. +func attemptContextHashFieldFromArray( + hash [AttemptContextHashFieldLength]byte, +) []byte { + out := make([]byte, AttemptContextHashFieldLength) + copy(out, hash[:]) + return out +} + +// attemptContextHashFieldToArray converts a wire-form slice back to +// a fixed-size 32-byte hash plus a presence flag. Returns +// (zeroArray, false) when the field is absent. Caller has already +// validated length via validateAttemptContextHashField; this function +// trusts that invariant and panics on violation. +func attemptContextHashFieldToArray( + field []byte, +) ([AttemptContextHashFieldLength]byte, bool) { + var out [AttemptContextHashFieldLength]byte + if len(field) == 0 { + return out, false + } + if len(field) != AttemptContextHashFieldLength { + panic(fmt.Sprintf( + "attemptContextHashFieldToArray called with wrong-length field [%d]", + len(field), + )) + } + copy(out[:], field) + return out, true +} diff --git a/pkg/frost/signing/attempt_context_binding_test.go b/pkg/frost/signing/attempt_context_binding_test.go new file mode 100644 index 0000000000..915a5dca2b --- /dev/null +++ b/pkg/frost/signing/attempt_context_binding_test.go @@ -0,0 +1,208 @@ +//go:build frost_native + +package signing + +import ( + "bytes" + "encoding/json" + "strings" + "testing" +) + +var pinnedAttemptContextHash = [AttemptContextHashFieldLength]byte{ + 0x00, 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, +} + +func TestValidateAttemptContextHashField_AcceptsAbsentOrCorrectLength(t *testing.T) { + tests := []struct { + name string + input []byte + }{ + {name: "nil is absent", input: nil}, + {name: "empty slice is absent", input: []byte{}}, + { + name: "exact length is accepted", + input: pinnedAttemptContextHash[:], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateAttemptContextHashField(tt.input); err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestValidateAttemptContextHashField_RejectsWrongLength(t *testing.T) { + tests := []struct { + name string + length int + }{ + {name: "too short", length: 31}, + {name: "too long", length: 33}, + {name: "one byte", length: 1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateAttemptContextHashField( + bytes.Repeat([]byte{0xff}, tt.length), + ) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "wrong length") { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} + +func TestAttemptContextHashField_ArrayRoundTrip(t *testing.T) { + field := attemptContextHashFieldFromArray(pinnedAttemptContextHash) + if len(field) != AttemptContextHashFieldLength { + t.Fatalf( + "expected length %d, got %d", + AttemptContextHashFieldLength, len(field), + ) + } + got, present := attemptContextHashFieldToArray(field) + if !present { + t.Fatal("expected presence=true") + } + if got != pinnedAttemptContextHash { + t.Fatalf("array round-trip mismatch: got %x want %x", got, pinnedAttemptContextHash) + } +} + +func TestAttemptContextHashField_ArrayToArrayAbsent(t *testing.T) { + got, present := attemptContextHashFieldToArray(nil) + if present { + t.Fatal("expected presence=false for nil") + } + var zero [AttemptContextHashFieldLength]byte + if got != zero { + t.Fatalf("expected zero array, got %x", got) + } +} + +func TestAttemptContextHashField_FromArrayDoesNotAliasCaller(t *testing.T) { + arr := pinnedAttemptContextHash + field := attemptContextHashFieldFromArray(arr) + field[0] = 0xff + if arr[0] == 0xff { + t.Fatal("mutation through returned slice modified caller's array") + } +} + +func TestBuildTaggedTBTCSignerRoundContributionMessage_OptionalFieldRoundTrip(t *testing.T) { + withHash := &buildTaggedTBTCSignerRoundContributionMessage{ + SenderIDValue: 3, + SessionIDValue: "session-3", + ContributionIdentifier: 1, + ContributionData: []byte{0xee, 0xff}, + } + withHash.SetAttemptContextHash(pinnedAttemptContextHash) + data, err := withHash.Marshal() + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + decoded := &buildTaggedTBTCSignerRoundContributionMessage{} + if err := decoded.Unmarshal(data); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + got, present := decoded.GetAttemptContextHash() + if !present || got != pinnedAttemptContextHash { + t.Fatalf("round-trip lost hash: present=%v got=%x", present, got) + } +} + +func TestBuildTaggedTBTCSignerRoundContributionMessage_BackwardCompatWithOldJSON(t *testing.T) { + oldJSON := []byte(`{ + "senderID":3, + "sessionID":"session-3", + "contributionIdentifier":1, + "contributionData":"qrs=" + }`) + + decoded := &buildTaggedTBTCSignerRoundContributionMessage{} + if err := decoded.Unmarshal(oldJSON); err != nil { + t.Fatalf("unmarshal of old-format JSON failed: %v", err) + } + if _, present := decoded.GetAttemptContextHash(); present { + t.Fatal("expected absent hash for old-format JSON") + } +} + +func TestBuildTaggedTBTCSignerRoundContributionMessage_RejectsWrongLengthHashField(t *testing.T) { + badJSON := []byte(`{ + "senderID":3, + "sessionID":"session-3", + "contributionIdentifier":1, + "contributionData":"qrs=", + "attemptContextHash":"AAEC" + }`) + + decoded := &buildTaggedTBTCSignerRoundContributionMessage{} + err := decoded.Unmarshal(badJSON) + if err == nil { + t.Fatal("expected wrong-length validation error") + } +} + +func TestBuildTaggedTBTCSignerRoundContributionMessagesEqual_HashFieldDifferentiates(t *testing.T) { + base := &buildTaggedTBTCSignerRoundContributionMessage{ + SenderIDValue: 1, + SessionIDValue: "session-1", + ContributionIdentifier: 1, + ContributionData: []byte{0xaa}, + } + withHashA := *base + withHashA.SetAttemptContextHash(pinnedAttemptContextHash) + + otherHash := pinnedAttemptContextHash + otherHash[0] ^= 0xff + withHashB := *base + withHashB.SetAttemptContextHash(otherHash) + + if buildTaggedTBTCSignerRoundContributionMessagesEqual(base, &withHashA) { + t.Fatal("base (no hash) vs with-hash must compare unequal") + } + if buildTaggedTBTCSignerRoundContributionMessagesEqual(&withHashA, &withHashB) { + t.Fatal("messages with different hashes must compare unequal") + } + withHashAClone := *base + withHashAClone.SetAttemptContextHash(pinnedAttemptContextHash) + if !buildTaggedTBTCSignerRoundContributionMessagesEqual(&withHashA, &withHashAClone) { + t.Fatal("messages with the same hash must compare equal") + } + if !buildTaggedTBTCSignerRoundContributionMessagesEqual(base, base) { + t.Fatal("identical-pointer comparison must be equal") + } +} + +func TestBuildTaggedTBTCSignerRoundContributionMessage_JSONEncoderOmitsAbsentField(t *testing.T) { + original := &buildTaggedTBTCSignerRoundContributionMessage{ + SenderIDValue: 1, + SessionIDValue: "s", + ContributionIdentifier: 1, + ContributionData: []byte{0xaa}, + } + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + var raw map[string]any + if err := json.Unmarshal(data, &raw); err != nil { + t.Fatalf("re-decode failed: %v", err) + } + if _, ok := raw["attemptContextHash"]; ok { + t.Fatalf( + "omitempty did not suppress absent attemptContextHash; raw=%v", + raw, + ) + } +} diff --git a/pkg/frost/signing/attempt_context_binding_validation_default_build_test.go b/pkg/frost/signing/attempt_context_binding_validation_default_build_test.go new file mode 100644 index 0000000000..288758f241 --- /dev/null +++ b/pkg/frost/signing/attempt_context_binding_validation_default_build_test.go @@ -0,0 +1,34 @@ +//go:build frost_native && !frost_roast_retry + +package signing + +import ( + "testing" +) + +func TestVerifyMessageAttemptContextHash_DefaultBuildPassesEverything(t *testing.T) { + // Without the frost_roast_retry tag, currentAttemptHandleForCollect + // always returns ok=false, so the helper short-circuits to nil + // for every input. This guarantees that the receive-loop wiring + // never enforces the AttemptContextHash binding in the default + // build, matching the rollback promise made in the rollout + // guide (docs/development/frost-roast-retry-rollout.adoc). + msg := stubDefaultBuildMessage{} + if err := verifyMessageAttemptContextHash(msg, "any-session"); err != nil { + t.Fatalf( + "default build must always pass; got %v", + err, + ) + } +} + +// stubDefaultBuildMessage is the equivalent of the tagged-build +// test's stubMessage. Kept separate to avoid the tagged-build +// definition leaking into this build's compilation unit. +type stubDefaultBuildMessage struct{} + +func (stubDefaultBuildMessage) GetAttemptContextHash() ( + [AttemptContextHashFieldLength]byte, bool, +) { + return [AttemptContextHashFieldLength]byte{}, false +} diff --git a/pkg/frost/signing/attempt_context_binding_validation_frost_native.go b/pkg/frost/signing/attempt_context_binding_validation_frost_native.go new file mode 100644 index 0000000000..715e030874 --- /dev/null +++ b/pkg/frost/signing/attempt_context_binding_validation_frost_native.go @@ -0,0 +1,105 @@ +//go:build frost_native + +package signing + +import ( + "errors" + "fmt" +) + +// attemptContextHashCarrier is implemented by every protocol +// message type that carries the optional AttemptContextHash field +// introduced in RFC-21 Phase 1B. The validation helper below uses +// the interface so a single implementation covers all three +// FROST/tbtc-signer message types without duplicating per-type +// logic. +type attemptContextHashCarrier interface { + // GetAttemptContextHash returns the message's hash and a + // presence flag. Implementations are generated by the per-type + // Set/Get helpers in attempt_context_binding.go. + GetAttemptContextHash() ([AttemptContextHashFieldLength]byte, bool) +} + +// outboundAttemptContextHashCarrier is implemented by every protocol +// message type that can carry the optional AttemptContextHash field +// on outbound sends. +type outboundAttemptContextHashCarrier interface { + SetAttemptContextHash([AttemptContextHashFieldLength]byte) +} + +// ErrAttemptContextHashMissing is returned when a message lacks +// the AttemptContextHash field while the session is bound to a +// ROAST attempt that requires it. Distinct sentinel so callers +// can map it to a specific RecordReject reason. +var ErrAttemptContextHashMissing = errors.New( + "attempt context hash required: session is ROAST-active but message omits the binding field", +) + +// ErrAttemptContextHashMismatch is returned when a message's +// AttemptContextHash does not match the session's currently-bound +// AttemptContext.Hash(). The peer is either talking about a stale +// attempt (post-transition) or trying to inject a message for a +// different context. +var ErrAttemptContextHashMismatch = errors.New( + "attempt context hash mismatch: message bound to a different attempt", +) + +// verifyMessageAttemptContextHash enforces the RFC-21 Phase-6 +// milestone that promotes the AttemptContextHash field from +// optional to required at the receive boundary, but only when the +// session has a ROAST-attempt binding registered. +// +// When no session-handle binding exists for sessionID (the typical +// state for non-ROAST sessions and for default builds), this +// function returns nil and lets the message through. The receive +// loop's other gates (shouldAcceptNativeFROSTMessage, etc.) still +// apply. +// +// When a binding exists -- i.e. the orchestration layer has begun +// an attempt for this session and is expecting the receive loops +// to participate -- the message must carry an AttemptContextHash +// that equals the bound context's Hash(). Returns +// ErrAttemptContextHashMissing or ErrAttemptContextHashMismatch on +// failure so the caller can RecordReject with a precise reason. +func verifyMessageAttemptContextHash( + msg attemptContextHashCarrier, + sessionID string, +) error { + _, ctx, ok := currentAttemptHandleForCollect(sessionID) + if !ok { + // No binding: legacy / non-ROAST mode. Skip enforcement + // so default builds and non-ROAST sessions stay + // observationally identical to pre-Phase-6 behaviour. + return nil + } + msgHash, present := msg.GetAttemptContextHash() + if !present { + return ErrAttemptContextHashMissing + } + expected := ctx.Hash() + if msgHash != expected { + return fmt.Errorf( + "%w: message=%x, current attempt=%x", + ErrAttemptContextHashMismatch, + msgHash[:4], + expected[:4], + ) + } + return nil +} + +// setMessageAttemptContextHashIfBound attaches the current ROAST +// attempt binding to an outbound message. Default/non-ROAST sessions +// have no binding, so the field stays absent for backward +// compatibility. +func setMessageAttemptContextHashIfBound( + msg outboundAttemptContextHashCarrier, + sessionID string, +) { + _, ctx, ok := currentAttemptHandleForCollect(sessionID) + if !ok { + return + } + + msg.SetAttemptContextHash(ctx.Hash()) +} diff --git a/pkg/frost/signing/attempt_context_binding_validation_frost_native_test.go b/pkg/frost/signing/attempt_context_binding_validation_frost_native_test.go new file mode 100644 index 0000000000..fcab1d38aa --- /dev/null +++ b/pkg/frost/signing/attempt_context_binding_validation_frost_native_test.go @@ -0,0 +1,227 @@ +//go:build frost_native && frost_roast_retry + +package signing + +import ( + "errors" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// stubMessage is a minimal attemptContextHashCarrier implementation +// for unit tests. The receive callbacks use the three real message +// types; the helper itself is exercised via this stub so the test +// surface stays small. +type stubMessage struct { + hash [AttemptContextHashFieldLength]byte + present bool +} + +func (s stubMessage) GetAttemptContextHash() ( + [AttemptContextHashFieldLength]byte, bool, +) { + return s.hash, s.present +} + +func (s *stubMessage) SetAttemptContextHash( + hash [AttemptContextHashFieldLength]byte, +) { + s.hash = hash + s.present = true +} + +func newOrchestrationTestContextForValidation(t *testing.T) attempt.AttemptContext { + t.Helper() + ctx, err := attempt.NewAttemptContext( + "validation-test", + "key-group", + []byte{0x01, 0x02}, + [attempt.MessageDigestLength]byte{0x77}, + 0, + []group.MemberIndex{1, 2, 3, 4, 5}, + nil, + ) + if err != nil { + t.Fatalf("ctx: %v", err) + } + return ctx +} + +func TestVerifyMessageAttemptContextHash_NoBindingPasses(t *testing.T) { + // In the default build, no session-handle bindings exist so + // every call returns nil regardless of message contents. The + // receive loop's other gates still apply. + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + cases := []stubMessage{ + {present: false}, + {present: true, hash: [AttemptContextHashFieldLength]byte{0x01}}, + } + for _, msg := range cases { + if err := verifyMessageAttemptContextHash(msg, "session-x"); err != nil { + t.Fatalf( + "no-binding path must pass; got %v for msg %+v", + err, msg, + ) + } + } +} + +func TestVerifyMessageAttemptContextHash_BindingPresent_MatchingHashPasses(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newOrchestrationTestContextForValidation(t) + SetCurrentAttemptHandleForSession("session-match", roast.AttemptHandle{}, ctx) + + expected := ctx.Hash() + msg := stubMessage{hash: expected, present: true} + if err := verifyMessageAttemptContextHash(msg, "session-match"); err != nil { + t.Fatalf("matching hash must pass; got %v", err) + } +} + +func TestVerifyMessageAttemptContextHash_BindingPresent_MissingHashFails(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newOrchestrationTestContextForValidation(t) + SetCurrentAttemptHandleForSession("session-missing", roast.AttemptHandle{}, ctx) + + msg := stubMessage{present: false} + err := verifyMessageAttemptContextHash(msg, "session-missing") + if !errors.Is(err, ErrAttemptContextHashMissing) { + t.Fatalf( + "expected ErrAttemptContextHashMissing; got %v", + err, + ) + } +} + +func TestVerifyMessageAttemptContextHash_BindingPresent_MismatchedHashFails(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newOrchestrationTestContextForValidation(t) + SetCurrentAttemptHandleForSession("session-mismatch", roast.AttemptHandle{}, ctx) + + wrong := [AttemptContextHashFieldLength]byte{} + for i := range wrong { + wrong[i] = 0xff + } + msg := stubMessage{hash: wrong, present: true} + err := verifyMessageAttemptContextHash(msg, "session-mismatch") + if !errors.Is(err, ErrAttemptContextHashMismatch) { + t.Fatalf( + "expected ErrAttemptContextHashMismatch; got %v", + err, + ) + } +} + +func TestVerifyMessageAttemptContextHash_RealMessageTypeIntegration(t *testing.T) { + // Exercise the helper against a real protocol message type + // (the tbtc-signer round contribution) rather than just the stub, + // so the test surface covers the actual Set/Get + // helpers code path. + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newOrchestrationTestContextForValidation(t) + SetCurrentAttemptHandleForSession("session-real-msg", roast.AttemptHandle{}, ctx) + + expected := ctx.Hash() + msg := &buildTaggedTBTCSignerRoundContributionMessage{ + SenderIDValue: 1, + SessionIDValue: "session-real-msg", + ContributionIdentifier: 1, + ContributionData: []byte{0x01}, + } + msg.SetAttemptContextHash(expected) + + if err := verifyMessageAttemptContextHash(msg, "session-real-msg"); err != nil { + t.Fatalf("real-message integration must pass; got %v", err) + } + + // Now mutate the context to break the binding. + differentCtx, _ := attempt.NewAttemptContext( + "session-real-msg", + "key-group", + []byte{0x99}, + [attempt.MessageDigestLength]byte{0x77}, + 1, + []group.MemberIndex{1, 2, 3, 4, 5}, + nil, + ) + SetCurrentAttemptHandleForSession("session-real-msg", roast.AttemptHandle{}, differentCtx) + + err := verifyMessageAttemptContextHash(msg, "session-real-msg") + if !errors.Is(err, ErrAttemptContextHashMismatch) { + t.Fatalf("rebinding must cause mismatch; got %v", err) + } +} + +func TestSetMessageAttemptContextHashIfBound_AttachesBoundHash(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newOrchestrationTestContextForValidation(t) + SetCurrentAttemptHandleForSession("session-outbound", roast.AttemptHandle{}, ctx) + + msg := &stubMessage{} + setMessageAttemptContextHashIfBound(msg, "session-outbound") + + got, present := msg.GetAttemptContextHash() + if !present { + t.Fatal("expected outbound message to carry attempt context hash") + } + if got != ctx.Hash() { + t.Fatalf("unexpected attempt context hash: got %x want %x", got, ctx.Hash()) + } +} + +func TestSetMessageAttemptContextHashIfBound_NoBindingLeavesAbsent(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + msg := &stubMessage{} + setMessageAttemptContextHashIfBound(msg, "session-no-binding") + + if _, present := msg.GetAttemptContextHash(); present { + t.Fatal("expected no attempt context hash without a session binding") + } +} + +func TestSetMessageAttemptContextHashIfBound_AllOutboundMessageTypes(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newOrchestrationTestContextForValidation(t) + SetCurrentAttemptHandleForSession("session-all-types", roast.AttemptHandle{}, ctx) + expected := ctx.Hash() + + messages := []attemptContextHashCarrier{ + &buildTaggedTBTCSignerRoundContributionMessage{}, + } + + for _, msg := range messages { + outbound, ok := msg.(outboundAttemptContextHashCarrier) + if !ok { + t.Fatalf("%T does not implement outbound carrier", msg) + } + + setMessageAttemptContextHashIfBound(outbound, "session-all-types") + + got, present := msg.GetAttemptContextHash() + if !present { + t.Fatalf("%T did not get attempt context hash", msg) + } + if got != expected { + t.Fatalf("%T hash mismatch: got %x want %x", msg, got, expected) + } + } +} diff --git a/pkg/frost/signing/attempt_context_bound_exchange_frost_native_test.go b/pkg/frost/signing/attempt_context_bound_exchange_frost_native_test.go new file mode 100644 index 0000000000..13b9580884 --- /dev/null +++ b/pkg/frost/signing/attempt_context_bound_exchange_frost_native_test.go @@ -0,0 +1,155 @@ +//go:build frost_native && frost_roast_retry + +package signing + +import ( + "context" + "math/big" + "sync" + "testing" + "time" + + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/net/local" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func bindAttemptContextHashForExchangeTest( + t *testing.T, + sessionID string, + members []group.MemberIndex, +) { + t.Helper() + + var messageDigest [attempt.MessageDigestLength]byte + copy(messageDigest[:], []byte("bound-attempt-context-exchange")) + + ctx, err := attempt.NewAttemptContext( + sessionID, + "key-group", + []byte{0x01, 0x02, 0x03}, + messageDigest, + 0, + members, + nil, + ) + if err != nil { + t.Fatalf("failed creating attempt context: [%v]", err) + } + + SetCurrentAttemptHandleForSession(sessionID, roast.AttemptHandle{}, ctx) +} + +func TestBuildTaggedTBTCSignerBootstrapCoarseRound_BoundAttemptContextHashExchange( + t *testing.T, +) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + provider := local.Connect() + channel, err := provider.BroadcastChannelFor( + "tbtc-signer-bootstrap-bound-attempt-context-test", + ) + if err != nil { + t.Fatalf("failed creating broadcast channel: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + primitive.RegisterUnmarshallers(channel) + + sessionID := "tbtc-signer-bound-attempt-context" + includedMembers := []group.MemberIndex{1, 2} + bindAttemptContextHashForExchangeTest(t, sessionID, includedMembers) + + engineByMember := map[group.MemberIndex]*deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{ + 1: { + roundState: &NativeTBTCSignerRoundState{ + SessionID: sessionID, + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "0011", + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: 1, + Data: []byte{0x11, 0x01}, + }, + }, + }, + 2: { + roundState: &NativeTBTCSignerRoundState{ + SessionID: sessionID, + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "0011", + OwnContribution: &NativeTBTCSignerRoundContribution{ + Identifier: 2, + Data: []byte{0x22, 0x02}, + }, + }, + }, + } + + requestByMember := map[group.MemberIndex]*NativeExecutionFFISigningRequest{ + 1: { + Message: big.NewInt(123), + SessionID: sessionID, + MemberIndex: 1, + GroupSize: 2, + DishonestThreshold: 1, + Channel: channel, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: includedMembers, + }, + }, + 2: { + Message: big.NewInt(123), + SessionID: sessionID, + MemberIndex: 2, + GroupSize: 2, + DishonestThreshold: 1, + Channel: channel, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: includedMembers, + }, + }, + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelCtx() + + signingErrors := make(chan error, len(requestByMember)) + var wg sync.WaitGroup + wg.Add(len(requestByMember)) + + for memberIndex, request := range requestByMember { + engine := engineByMember[memberIndex] + go func( + signingRequest *NativeExecutionFFISigningRequest, + signingEngine NativeTBTCSignerEngine, + ) { + defer wg.Done() + + signingErrors <- executeBuildTaggedTBTCSignerBootstrapCoarseRound( + ctx, + signingRequest, + "group-1", + signingEngine, + nil, + nil, + ) + }(request, engine) + } + + wg.Wait() + close(signingErrors) + + for signErr := range signingErrors { + if signErr != nil { + t.Fatalf("unexpected signing error: [%v]", signErr) + } + } +} diff --git a/pkg/frost/signing/attempt_context_from_request.go b/pkg/frost/signing/attempt_context_from_request.go new file mode 100644 index 0000000000..70274d895c --- /dev/null +++ b/pkg/frost/signing/attempt_context_from_request.go @@ -0,0 +1,200 @@ +//go:build frost_native + +package signing + +import ( + "errors" + "fmt" + "math/big" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" +) + +// ErrAttemptContextConstruction is the sentinel error class returned +// by BuildAttemptContextFromRequest for any failure during +// construction. Callers can match with errors.Is to distinguish +// it from runtime ROAST errors. +var ErrAttemptContextConstruction = errors.New( + "attempt context: construction failed", +) + +// BuildAttemptContextFromRequest converts a +// NativeExecutionFFISigningRequest into an attempt.AttemptContext +// suitable for Coordinator.BeginAttempt. The conversion: +// +// - SessionID, AttemptNumber, IncludedSet, ExcludedSet come from +// the request and its Attempt sub-struct directly. +// - TransientlyParked is empty: the existing Attempt struct does +// not carry parking info. Phase-7+ orchestration that drives +// multi-attempt sessions will need to thread parking metadata +// through; Phase 6 only handles attempt-zero shape. +// - MessageDigest is the request.Message bytes left-padded with +// zeros to 32 bytes, then truncated if longer. In BIP-340 +// production, request.Message is already a 32-byte digest of +// the tagged payload, so padding is a no-op. +// - DkgGroupPublicKey is extracted via +// ExtractDkgGroupPublicKeyFromMaterial. +// - KeyGroupID is derived from the raw FrostTBTCSignerV1 KeyGroup string +// identifier, which is already a canonical per-group handle. +// - AttemptSeed = SHA256(DkgGroupPublicKey || SessionID || +// MessageDigest) per RFC-21 Decision 2. +// +// Critically, the FFI signer material is decoded *first* so any +// extraction failure is surfaced before the AttemptContext is +// constructed. This enforces the ordering Gemini flagged in the +// Phase-6 design review: AttemptContext must never be built from +// undecoded material because the seed derivation would silently +// fail. +// +// Returns ErrAttemptContextConstruction-wrapped errors for any +// failure during the construction. Returns ErrUnsupportedSignerMaterialFormat +// (via errors.Is) when the material's format is not extractable +// (e.g. FrostUniFFIV1 or unsupported FrostUniFFIV2 today). +func BuildAttemptContextFromRequest( + request *NativeExecutionFFISigningRequest, +) (attempt.AttemptContext, error) { + if request == nil { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: request is nil", + ErrAttemptContextConstruction, + ) + } + if request.Message == nil { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: request message is nil", + ErrAttemptContextConstruction, + ) + } + if request.SignerMaterial == nil { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: signer material is nil", + ErrAttemptContextConstruction, + ) + } + if request.Attempt == nil { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: attempt metadata is nil", + ErrAttemptContextConstruction, + ) + } + + // Strict ordering: extract DKG group public key (which decodes + // the signer material) BEFORE deriving the context. A failure + // here propagates directly without leaving a half-built + // context. + dkgPub, err := ExtractDkgGroupPublicKeyFromMaterial(request.SignerMaterial) + if err != nil { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: %w", + ErrAttemptContextConstruction, + err, + ) + } + + keyGroupID, err := deriveKeyGroupID(request.SignerMaterial, dkgPub) + if err != nil { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: %w", + ErrAttemptContextConstruction, + err, + ) + } + + digest, err := messageDigestFromBigInt(request.Message) + if err != nil { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: %w", + ErrAttemptContextConstruction, + err, + ) + } + + // AttemptNumber on the keep-core Attempt struct is 1-based + // (1 = first attempt). RFC-21's AttemptContext.AttemptNumber is + // 0-based. Convert by subtracting 1 (Attempt.Number must be + // >= 1). + if request.Attempt.Number == 0 { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: request.Attempt.Number is zero (must be >= 1)", + ErrAttemptContextConstruction, + ) + } + attemptNumber := uint32(request.Attempt.Number - 1) + + ctx, err := attempt.NewAttemptContextWithParking( + request.SessionID, + keyGroupID, + dkgPub, + digest, + attemptNumber, + request.Attempt.IncludedMembersIndexes, + request.Attempt.ExcludedMembersIndexes, + nil, // Phase 6 ships attempt-zero shape; parking lands in Phase 7+ orchestration. + ) + if err != nil { + return attempt.AttemptContext{}, fmt.Errorf( + "%w: %w", + ErrAttemptContextConstruction, + err, + ) + } + return ctx, nil +} + +// deriveKeyGroupID computes the AttemptContext KeyGroupID field +// from the signer material plus the already-extracted DKG group +// public key. The derivation is format-aware: +// +// - FrostTBTCSignerV1: the raw KeyGroup string from the tbtc- +// signer material. That string is the canonical handle. +// +// Returns an error for unknown formats; the caller will already +// have rejected unsupported formats via ExtractDkgGroupPublicKeyFromMaterial, +// so reaching the default arm here is an internal consistency +// error. +func deriveKeyGroupID( + signerMaterial *NativeSignerMaterial, + dkgPub []byte, +) (string, error) { + switch signerMaterial.Format { + case NativeSignerMaterialFormatFrostTBTCSignerV1: + payload, err := decodeBuildTaggedTBTCSignerMaterialPayload(signerMaterial) + if err != nil { + return "", fmt.Errorf("derive key group id: %w", err) + } + return payload.KeyGroup, nil + default: + return "", fmt.Errorf( + "derive key group id: cannot derive id from format %q", + signerMaterial.Format, + ) + } +} + +// messageDigestFromBigInt converts a *big.Int message to the +// 32-byte digest shape AttemptContext expects. Big-int values +// shorter than 32 bytes are left-padded with zeros (big.Int.Bytes +// strips leading zeros). Values longer than 32 bytes return an +// error -- a real digest never exceeds 32 bytes for SHA-256. +func messageDigestFromBigInt( + message *big.Int, +) ([attempt.MessageDigestLength]byte, error) { + var out [attempt.MessageDigestLength]byte + if message == nil { + return out, fmt.Errorf("message is nil") + } + bz := message.Bytes() + if len(bz) > attempt.MessageDigestLength { + return out, fmt.Errorf( + "message digest length %d exceeds expected %d", + len(bz), + attempt.MessageDigestLength, + ) + } + // Left-pad with zeros: big.Int.Bytes strips leading zeros, so a + // 32-byte digest with a leading zero byte returns a 31-byte + // slice. Copy into the tail of `out` to restore canonical + // alignment. + copy(out[attempt.MessageDigestLength-len(bz):], bz) + return out, nil +} diff --git a/pkg/frost/signing/attempt_context_from_request_test.go b/pkg/frost/signing/attempt_context_from_request_test.go new file mode 100644 index 0000000000..2de38d0f6c --- /dev/null +++ b/pkg/frost/signing/attempt_context_from_request_test.go @@ -0,0 +1,249 @@ +//go:build frost_native + +package signing + +import ( + "crypto/sha256" + "encoding/json" + "errors" + "fmt" + "math/big" + "strings" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func newTestRequestWithUnsupportedUniFFIV2Material(t *testing.T, attemptNumber uint) *NativeExecutionFFISigningRequest { + t.Helper() + const hexKey = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + payload := unsupportedUniFFIV2Payload(t, hexKey) + return &NativeExecutionFFISigningRequest{ + Message: new(big.Int).SetBytes([]byte{0xab, 0xcd}), + SessionID: "session-test", + MemberIndex: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV2, + Payload: payload, + }, + Attempt: &Attempt{ + Number: attemptNumber, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2, 3, 4, 5}, + ExcludedMembersIndexes: nil, + }, + } +} + +func newTestRequestWithTBTCSignerV1Material(t *testing.T, attemptNumber uint) *NativeExecutionFFISigningRequest { + t.Helper() + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: "tbtc-group-A", + }) + return &NativeExecutionFFISigningRequest{ + Message: new(big.Int).SetBytes([]byte{0xab, 0xcd}), + SessionID: "session-test", + MemberIndex: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + }, + Attempt: &Attempt{ + Number: attemptNumber, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2, 3}, + ExcludedMembersIndexes: nil, + }, + } +} + +func TestBuildAttemptContextFromRequest_TBTCSignerV1_KeyGroupIDIsRawIdentifier(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 1) + ctx, err := BuildAttemptContextFromRequest(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.KeyGroupID != "tbtc-group-A" { + t.Fatalf( + "key group id: got %q, want %q", + ctx.KeyGroupID, + "tbtc-group-A", + ) + } +} + +func TestBuildAttemptContextFromRequest_UnsupportedUniFFIV2Rejected(t *testing.T) { + req := newTestRequestWithUnsupportedUniFFIV2Material(t, 1) + _, err := BuildAttemptContextFromRequest(req) + if !errors.Is(err, ErrUnsupportedSignerMaterialFormat) { + t.Fatalf("expected ErrUnsupportedSignerMaterialFormat, got %v", err) + } + if !strings.Contains(err.Error(), "unsupported") { + t.Fatalf("error should mention unsupported format; got %v", err) + } +} + +func TestBuildAttemptContextFromRequest_RejectsNilRequest(t *testing.T) { + _, err := BuildAttemptContextFromRequest(nil) + if !errors.Is(err, ErrAttemptContextConstruction) { + t.Fatalf("expected ErrAttemptContextConstruction, got %v", err) + } +} + +func TestBuildAttemptContextFromRequest_RejectsNilMessage(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 1) + req.Message = nil + _, err := BuildAttemptContextFromRequest(req) + if err == nil { + t.Fatal("expected error for nil message") + } + if !strings.Contains(err.Error(), "message is nil") { + t.Fatalf("error must mention nil message; got %v", err) + } +} + +func TestBuildAttemptContextFromRequest_RejectsNilSignerMaterial(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 1) + req.SignerMaterial = nil + _, err := BuildAttemptContextFromRequest(req) + if err == nil { + t.Fatal("expected error for nil signer material") + } + if !strings.Contains(err.Error(), "signer material is nil") { + t.Fatalf("error must mention nil signer material; got %v", err) + } +} + +func TestBuildAttemptContextFromRequest_RejectsNilAttempt(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 1) + req.Attempt = nil + _, err := BuildAttemptContextFromRequest(req) + if err == nil { + t.Fatal("expected error for nil attempt metadata") + } +} + +func TestBuildAttemptContextFromRequest_RejectsZeroAttemptNumber(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 0) + _, err := BuildAttemptContextFromRequest(req) + if err == nil { + t.Fatal("expected error for zero attempt number") + } + if !strings.Contains(err.Error(), "Attempt.Number is zero") { + t.Fatalf("error must mention zero attempt; got %v", err) + } +} + +func TestBuildAttemptContextFromRequest_PropagatesExtractionErrors(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 1) + req.SignerMaterial = &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte("{}"), + } + _, err := BuildAttemptContextFromRequest(req) + if !errors.Is(err, ErrUnsupportedSignerMaterialFormat) { + t.Fatalf("expected ErrUnsupportedSignerMaterialFormat, got %v", err) + } + if !errors.Is(err, ErrAttemptContextConstruction) { + t.Fatalf("expected ErrAttemptContextConstruction wrapper, got %v", err) + } +} + +func TestBuildAttemptContextFromRequest_AttemptNumberIsZeroBased(t *testing.T) { + cases := []struct { + legacyNumber uint + expectedZeroBased uint32 + }{ + {1, 0}, + {2, 1}, + {5, 4}, + } + for _, tc := range cases { + t.Run(fmt.Sprintf("legacy=%d", tc.legacyNumber), func(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, tc.legacyNumber) + ctx, err := BuildAttemptContextFromRequest(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if ctx.AttemptNumber != tc.expectedZeroBased { + t.Fatalf( + "got attempt number %d, want %d (legacy 1-based input %d)", + ctx.AttemptNumber, tc.expectedZeroBased, tc.legacyNumber, + ) + } + }) + } +} + +func TestMessageDigestFromBigInt_PadsShortBigInts(t *testing.T) { + bi := new(big.Int).SetBytes([]byte{0x01, 0x02}) + digest, err := messageDigestFromBigInt(bi) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := [attempt.MessageDigestLength]byte{} + want[30] = 0x01 + want[31] = 0x02 + if digest != want { + t.Fatalf("padding wrong: got %x, want %x", digest, want) + } +} + +func TestMessageDigestFromBigInt_RejectsLongBigInts(t *testing.T) { + bi := new(big.Int).SetBytes(make([]byte, 33)) + bi.SetBit(bi, 264, 1) // 33-byte length + _, err := messageDigestFromBigInt(bi) + if err == nil { + t.Fatal("expected error for over-long message") + } +} + +func TestBuildAttemptContextFromRequest_DeterministicAcrossInvocations(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 1) + a, err := BuildAttemptContextFromRequest(req) + if err != nil { + t.Fatalf("first: %v", err) + } + b, err := BuildAttemptContextFromRequest(req) + if err != nil { + t.Fatalf("second: %v", err) + } + if a.Hash() != b.Hash() { + t.Fatalf( + "two calls with same request produced different hashes: %x vs %x", + a.Hash(), b.Hash(), + ) + } +} + +func TestBuildAttemptContextFromRequest_HashChangesWhenMessageDigestChanges(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 1) + a, _ := BuildAttemptContextFromRequest(req) + req.Message = new(big.Int).SetBytes([]byte{0x99, 0x88, 0x77}) + b, _ := BuildAttemptContextFromRequest(req) + if a.Hash() == b.Hash() { + t.Fatal("hash must change when message digest changes") + } +} + +func TestBuildAttemptContextFromRequest_HashChangesWhenIncludedSetChanges(t *testing.T) { + req := newTestRequestWithTBTCSignerV1Material(t, 1) + a, _ := BuildAttemptContextFromRequest(req) + req.Attempt.IncludedMembersIndexes = []group.MemberIndex{1, 2, 4} + b, _ := BuildAttemptContextFromRequest(req) + if a.Hash() == b.Hash() { + t.Fatal("hash must change when included set changes") + } +} + +// Sanity check that the message digest padding produces the same +// bytes as a direct SHA-256 (just a smoke test on the constants). +func TestMessageDigestFromBigInt_SmokeTestSha256Length(t *testing.T) { + if attempt.MessageDigestLength != sha256.Size { + t.Fatalf( + "AttemptContext digest length %d != SHA-256 size %d", + attempt.MessageDigestLength, sha256.Size, + ) + } +} 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/backend.go b/pkg/frost/signing/backend.go new file mode 100644 index 0000000000..4bf01e76a3 --- /dev/null +++ b/pkg/frost/signing/backend.go @@ -0,0 +1,241 @@ +package signing + +import ( + "context" + "fmt" + "strings" + "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) +} + +type nativeExecutionAvailabilityReporter interface { + NativeExecutionAvailable() bool +} + +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", + ) + + // 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 + nativeExecutionFFISigningPrimitiveProviderForBuild NativeExecutionFFISigningPrimitiveProviderForBuild + nativeExecutionMode = nativeExecutionModeFallbackAllowed +) + +// 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 + +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() + + 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() + nativeExecutionMode = nativeExecutionModeFallbackAllowed +} + +// CurrentExecutionBackendName returns the active backend name. +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": 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": + 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 nil + case "ffi": + previousMode := currentNativeExecutionMode() + setNativeExecutionMode(nativeExecutionModeStrict) + + nativeBackend, err := currentNativeExecutionBackend() + if err != nil { + setNativeExecutionMode(previousMode) + return err + } + + 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() + + 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 { + 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 +} + +// 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() + RegisterNativeExecutionFFISigningPrimitiveForBuild() +} + +func currentNativeExecutionBackend() (ExecutionBackend, error) { + executionBackendMutex.RLock() + adapter := nativeExecutionAdapter + mode := nativeExecutionMode + executionBackendMutex.RUnlock() + + if adapter == nil { + return nil, fmt.Errorf( + "%w: no native execution adapter registered", + ErrNativeExecutionBackendUnavailable, + ) + } + + 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( + "%w: [%v]", + ErrNativeExecutionBackendUnavailable, + err, + ) + } + + return backend, nil +} diff --git a/pkg/frost/signing/backend_test.go b/pkg/frost/signing/backend_test.go new file mode 100644 index 0000000000..3a20dac2c9 --- /dev/null +++ b/pkg/frost/signing/backend_test.go @@ -0,0 +1,530 @@ +package signing + +import ( + "context" + "errors" + "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 +} + +type mockNativeExecutionAdapter struct { + executeCalls int + lastRequest *Request + result *Result + err error + + registerUnmarshallersCalls int + lastChannel net.BroadcastChannel +} + +type mockNativeExecutionAdapterWithAvailability struct { + *mockNativeExecutionAdapter + nativeExecutionAvailable bool +} + +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 (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 (mneawa *mockNativeExecutionAdapterWithAvailability) NativeExecutionAvailable() bool { + return mneawa.nativeExecutionAvailable +} + +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 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) + } + 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 !errors.Is(err, ErrNativeExecutionBackendUnavailable) { + t.Fatalf( + "unexpected native backend error\\nexpected: [%v]\\nactual: [%v]", + ErrNativeExecutionBackendUnavailable, + 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 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") + if err == nil { + t.Fatal("expected unknown backend error") + } +} + +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() + UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(ResetExecutionBackend) + t.Cleanup(UnregisterNativeExecutionAdapter) + t.Cleanup(UnregisterNativeExecutionBridge) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + + 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(), + ) + } + 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(), + 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 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{}, + 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") + } +} + +func TestRegisterNativeExecutionBridge_Nil(t *testing.T) { + if err := RegisterNativeExecutionBridge(nil); err == nil { + t.Fatal("expected nil native bridge error") + } +} + +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) + + 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/dkg_group_pubkey_extraction.go b/pkg/frost/signing/dkg_group_pubkey_extraction.go new file mode 100644 index 0000000000..db4b3880ae --- /dev/null +++ b/pkg/frost/signing/dkg_group_pubkey_extraction.go @@ -0,0 +1,187 @@ +//go:build frost_native + +package signing + +import ( + "encoding/hex" + "errors" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/keep-network/keep-core/pkg/frost" +) + +// ErrUnsupportedSignerMaterialFormat is returned by +// ExtractDkgGroupPublicKeyFromMaterial when the material's Format +// field names a signer-material variant the helper cannot extract a DKG group +// public key from. The current implementation accepts FrostTBTCSignerV1; +// FrostUniFFIV1 is rejected because the legacy bridge format does not expose +// the group key, and unsupported FrostUniFFIV2 material is rejected because it +// cannot support Taproot-tweaked deposit sweep signatures. +// +// Per RFC-21 Phase-6 Resolved Decision: the Phase 7 manifest flip +// is gated on verified migration off V1 across production signers, +// so this error class is expected to disappear by the time ROAST +// retry ships unconditionally. +var ErrUnsupportedSignerMaterialFormat = errors.New( + "dkg group public key: unsupported signer-material format for extraction", +) + +// ExtractDkgGroupPublicKeyFromMaterial returns the DKG-validated +// group public key from the supplied NativeSignerMaterial in the +// canonical byte representation that attempt.DeriveAttemptSeed +// consumes. Two honest signers feeding the same material into this +// helper produce byte-identical outputs. +// +// Format handling: +// +// - FrostTBTCSignerV1: decode payload as NativeTBTCSignerMaterialPayload; +// return the raw bytes of the KeyGroup identifier. The tbtc-signer +// engine treats KeyGroup as the canonical handle for the FROST +// key group; every honest signer running the same tbtc-signer +// build agrees on its bytes. +// +// - FrostUniFFIV1 and FrostUniFFIV2: return +// ErrUnsupportedSignerMaterialFormat. V1 material is the legacy bridge +// format that does not carry the group key in a form Phase 6 can extract. +// V2 material is unsupported in favor of FrostTBTCSignerV1. +// +// Callers MUST use the returned bytes only as the +// DkgGroupPublicKey input to attempt.DeriveAttemptSeed; the bytes +// are not interchangeable across format boundaries. Production signing groups +// must use FrostTBTCSignerV1 material. +func ExtractDkgGroupPublicKeyFromMaterial( + signerMaterial *NativeSignerMaterial, +) ([]byte, error) { + if signerMaterial == nil { + return nil, fmt.Errorf( + "dkg group public key: signer material is nil", + ) + } + switch signerMaterial.Format { + case NativeSignerMaterialFormatFrostTBTCSignerV1: + return extractDkgGroupPublicKeyFromTBTCSignerV1(signerMaterial) + case NativeSignerMaterialFormatFrostUniFFIV1: + return nil, fmt.Errorf( + "%w: %s (migrate to %s before enabling ROAST retry)", + ErrUnsupportedSignerMaterialFormat, + signerMaterial.Format, + NativeSignerMaterialFormatFrostTBTCSignerV1, + ) + case NativeSignerMaterialFormatFrostUniFFIV2: + return nil, fmt.Errorf( + "%w: %s is unsupported; use %s", + ErrUnsupportedSignerMaterialFormat, + signerMaterial.Format, + NativeSignerMaterialFormatFrostTBTCSignerV1, + ) + default: + return nil, fmt.Errorf( + "%w: unknown format %q", + ErrUnsupportedSignerMaterialFormat, + signerMaterial.Format, + ) + } +} + +// ExtractTaprootOutputKeyFromMaterial returns the 32-byte x-only Taproot +// output key committed to by native FROST signer material. +func ExtractTaprootOutputKeyFromMaterial( + signerMaterial *NativeSignerMaterial, +) ([]byte, error) { + if signerMaterial == nil { + return nil, fmt.Errorf("taproot output key: signer material is nil") + } + + switch signerMaterial.Format { + case NativeSignerMaterialFormatFrostTBTCSignerV1: + return extractTaprootOutputKeyFromTBTCSignerV1(signerMaterial) + default: + return nil, fmt.Errorf( + "taproot output key: unsupported signer-material format [%s]", + signerMaterial.Format, + ) + } +} + +func extractDkgGroupPublicKeyFromTBTCSignerV1( + signerMaterial *NativeSignerMaterial, +) ([]byte, error) { + payload, err := decodeBuildTaggedTBTCSignerMaterialPayload(signerMaterial) + if err != nil { + return nil, fmt.Errorf( + "dkg group public key: decode FrostTBTCSignerV1: %w", + err, + ) + } + if payload.KeyGroup == "" { + return nil, fmt.Errorf( + "dkg group public key: FrostTBTCSignerV1 key group is empty", + ) + } + return []byte(payload.KeyGroup), nil +} + +func extractTaprootOutputKeyFromTBTCSignerV1( + signerMaterial *NativeSignerMaterial, +) ([]byte, error) { + payload, err := decodeBuildTaggedTBTCSignerMaterialPayload(signerMaterial) + if err != nil { + return nil, fmt.Errorf( + "taproot output key: decode FrostTBTCSignerV1: %w", + err, + ) + } + if payload.KeyGroupSource != NativeTBTCSignerKeyGroupSourceDKGPersisted { + return nil, fmt.Errorf( + "taproot output key: FrostTBTCSignerV1 key group source [%s] is not [%s]", + payload.KeyGroupSource, + NativeTBTCSignerKeyGroupSourceDKGPersisted, + ) + } + + outputKeyHex := payload.TaprootOutputKey + if outputKeyHex == "" { + outputKeyHex = payload.KeyGroup + } + + outputKey, err := TaprootOutputKeyFromTBTCSignerKey(outputKeyHex) + if err != nil { + return nil, fmt.Errorf( + "taproot output key: FrostTBTCSignerV1 key material is invalid: %w", + err, + ) + } + + return outputKey, nil +} + +// TaprootOutputKeyFromTBTCSignerKey converts tbtc-signer key material to the +// x-only BIP-340 output key committed to by P2TR wallet scripts. Current +// tbtc-signer DKG results expose the group verifying key as a compressed +// secp256k1 key-group handle, while older test material may already carry the +// x-only key. +func TaprootOutputKeyFromTBTCSignerKey(keyHex string) ([]byte, error) { + raw, err := hex.DecodeString(keyHex) + if err != nil { + return nil, err + } + + switch len(raw) { + case frost.OutputKeySize: + return raw, nil + case 1 + frost.OutputKeySize: + publicKey, err := btcec.ParsePubKey(raw) + if err != nil { + return nil, err + } + return publicKey.X().FillBytes(make([]byte, frost.OutputKeySize)), nil + default: + return nil, fmt.Errorf( + "must be %d-byte x-only or %d-byte compressed key, got %d bytes", + frost.OutputKeySize, + 1+frost.OutputKeySize, + len(raw), + ) + } +} diff --git a/pkg/frost/signing/dkg_group_pubkey_extraction_test.go b/pkg/frost/signing/dkg_group_pubkey_extraction_test.go new file mode 100644 index 0000000000..13525bea2b --- /dev/null +++ b/pkg/frost/signing/dkg_group_pubkey_extraction_test.go @@ -0,0 +1,238 @@ +//go:build frost_native + +package signing + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "strings" + "testing" +) + +func TestExtractDkgGroupPublicKey_RejectsNilMaterial(t *testing.T) { + _, err := ExtractDkgGroupPublicKeyFromMaterial(nil) + if err == nil { + t.Fatal("expected error for nil material") + } +} + +func TestExtractDkgGroupPublicKey_FrostUniFFIV2_ReturnsUnsupportedSentinel(t *testing.T) { + payload := unsupportedUniFFIV2Payload( + t, + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + ) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV2, + Payload: payload, + } + _, err := ExtractDkgGroupPublicKeyFromMaterial(mat) + if !errors.Is(err, ErrUnsupportedSignerMaterialFormat) { + t.Fatalf("expected ErrUnsupportedSignerMaterialFormat, got %v", err) + } + if !strings.Contains(err.Error(), "unsupported") { + t.Fatalf("error must mention unsupported format; got %v", err) + } +} + +func TestExtractTaprootOutputKey_FrostUniFFIV2_ReturnsUnsupported(t *testing.T) { + payload := unsupportedUniFFIV2Payload( + t, + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + ) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV2, + Payload: payload, + } + + _, err := ExtractTaprootOutputKeyFromMaterial(mat) + if err == nil { + t.Fatal("expected unsupported V2 taproot output key rejection") + } + if !strings.Contains(err.Error(), "unsupported signer-material format") { + t.Fatalf("error must mention unsupported format; got %v", err) + } +} + +func TestExtractDkgGroupPublicKey_FrostTBTCSignerV1_ReturnsKeyGroupBytes(t *testing.T) { + const keyGroup = "group-A" + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: keyGroup, + }) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + got, err := ExtractDkgGroupPublicKeyFromMaterial(mat) + if err != nil { + t.Fatalf("extract: %v", err) + } + if string(got) != keyGroup { + t.Fatalf("got %q, want %q", string(got), keyGroup) + } +} + +func TestExtractTaprootOutputKey_FrostTBTCSignerV1_DKGPersistedHexDecodes( + t *testing.T, +) { + const hexKey = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: hexKey, + KeyGroupSource: NativeTBTCSignerKeyGroupSourceDKGPersisted, + }) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + + got, err := ExtractTaprootOutputKeyFromMaterial(mat) + if err != nil { + t.Fatalf("extract: %v", err) + } + want, _ := hex.DecodeString(hexKey) + if !bytes.Equal(got, want) { + t.Fatalf( + "taproot output key mismatch: got %x, want %x", + got, + want, + ) + } +} + +func TestExtractTaprootOutputKey_FrostTBTCSignerV1_DKGPersistedCompressedKeyGroup( + t *testing.T, +) { + const compressedKey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + const xOnlyKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: compressedKey, + KeyGroupSource: NativeTBTCSignerKeyGroupSourceDKGPersisted, + }) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + + got, err := ExtractTaprootOutputKeyFromMaterial(mat) + if err != nil { + t.Fatalf("extract: %v", err) + } + want, _ := hex.DecodeString(xOnlyKey) + if !bytes.Equal(got, want) { + t.Fatalf( + "taproot output key mismatch: got %x, want %x", + got, + want, + ) + } +} + +func TestExtractTaprootOutputKey_FrostTBTCSignerV1_DKGPersistedUsesExplicitOutputKey( + t *testing.T, +) { + const compressedKey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + const outputKey = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: compressedKey, + TaprootOutputKey: outputKey, + KeyGroupSource: NativeTBTCSignerKeyGroupSourceDKGPersisted, + }) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + + got, err := ExtractTaprootOutputKeyFromMaterial(mat) + if err != nil { + t.Fatalf("extract: %v", err) + } + want, _ := hex.DecodeString(outputKey) + if !bytes.Equal(got, want) { + t.Fatalf( + "taproot output key mismatch: got %x, want %x", + got, + want, + ) + } +} + +func TestExtractTaprootOutputKey_FrostTBTCSignerV1_RejectsNonDKGSource( + t *testing.T, +) { + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: strings.Repeat("11", 32), + KeyGroupSource: NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + }) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + + _, err := ExtractTaprootOutputKeyFromMaterial(mat) + if err == nil { + t.Fatal("expected non-DKG source rejection") + } + if !strings.Contains(err.Error(), NativeTBTCSignerKeyGroupSourceDKGPersisted) { + t.Fatalf("error should mention persisted DKG source: [%v]", err) + } +} + +func TestExtractDkgGroupPublicKey_FrostTBTCSignerV1_DeterministicAcrossCalls(t *testing.T) { + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: "deterministic-group", + }) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + a, _ := ExtractDkgGroupPublicKeyFromMaterial(mat) + b, _ := ExtractDkgGroupPublicKeyFromMaterial(mat) + if !bytes.Equal(a, b) { + t.Fatalf("extraction is non-deterministic: %x vs %x", a, b) + } +} + +func TestExtractDkgGroupPublicKey_FrostTBTCSignerV1_RejectsEmptyKeyGroup(t *testing.T) { + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: "", + }) + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + _, err := ExtractDkgGroupPublicKeyFromMaterial(mat) + if err == nil { + t.Fatal("expected error for empty KeyGroup") + } +} + +func TestExtractDkgGroupPublicKey_FrostUniFFIV1_ReturnsUnsupportedSentinel(t *testing.T) { + mat := &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte("{}"), + } + _, err := ExtractDkgGroupPublicKeyFromMaterial(mat) + if !errors.Is(err, ErrUnsupportedSignerMaterialFormat) { + t.Fatalf("expected ErrUnsupportedSignerMaterialFormat, got %v", err) + } + if !strings.Contains(err.Error(), "migrate to") { + t.Fatalf("error must guide operator to migration; got %v", err) + } +} + +func TestExtractDkgGroupPublicKey_UnknownFormat_ReturnsUnsupportedSentinel(t *testing.T) { + mat := &NativeSignerMaterial{ + Format: "frost-some-future-format-v0", + Payload: []byte("{}"), + } + _, err := ExtractDkgGroupPublicKeyFromMaterial(mat) + if !errors.Is(err, ErrUnsupportedSignerMaterialFormat) { + t.Fatalf("expected ErrUnsupportedSignerMaterialFormat, got %v", err) + } + if !strings.Contains(err.Error(), "frost-some-future-format-v0") { + t.Fatalf("error must mention the unknown format; got %v", err) + } +} diff --git a/pkg/frost/signing/evidence_overflow.go b/pkg/frost/signing/evidence_overflow.go new file mode 100644 index 0000000000..60bbe89cc5 --- /dev/null +++ b/pkg/frost/signing/evidence_overflow.go @@ -0,0 +1,43 @@ +//go:build frost_native + +package signing + +import ( + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// senderIndexedMessage is the minimal contract a protocol message must +// satisfy for enqueueOrRecordOverflow to handle it: the message must +// expose its sender so the recorder can attribute overflow events to a +// specific member. +type senderIndexedMessage interface { + SenderID() group.MemberIndex +} + +// enqueueOrRecordOverflow attempts to enqueue payload onto target. If +// the channel is full, the overflow is recorded against the payload's +// sender on the supplied recorder instead. Returns true if the payload +// was enqueued, false if the overflow was recorded. +// +// This is the shared select-or-record body that replaces the three +// inline select { default } drop sites in the FROST/tbtc-signer +// receive loops. Pulling it out lets the recorder integration be unit- +// tested directly without spinning up a network channel. +// +// Phase 2 callers pass attempt.NoOpRecorder(), so behaviour is +// observably unchanged from before RFC-21 wiring. A coordinator-aware +// caller in a later phase injects a real recorder. +func enqueueOrRecordOverflow[T senderIndexedMessage]( + payload T, + target chan<- T, + recorder attempt.EvidenceRecorder, +) bool { + select { + case target <- payload: + return true + default: + recorder.RecordOverflow(payload.SenderID()) + return false + } +} diff --git a/pkg/frost/signing/evidence_overflow_test.go b/pkg/frost/signing/evidence_overflow_test.go new file mode 100644 index 0000000000..15a70a48e6 --- /dev/null +++ b/pkg/frost/signing/evidence_overflow_test.go @@ -0,0 +1,126 @@ +//go:build frost_native + +package signing + +import ( + "sync" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestEnqueueOrRecordOverflow_EnqueuesWhenChannelHasRoom(t *testing.T) { + ch := make(chan *buildTaggedTBTCSignerRoundContributionMessage, 4) + rec := attempt.NewBoundedRecorder() + payload := &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: 1} + + if !enqueueOrRecordOverflow(payload, ch, rec) { + t.Fatal("enqueue should succeed when channel has room") + } + if got := rec.Snapshot().Overflows[1]; got != 0 { + t.Fatalf("no overflow expected on successful enqueue; got %d", got) + } + if len(ch) != 1 { + t.Fatalf("channel length expected 1, got %d", len(ch)) + } +} + +func TestEnqueueOrRecordOverflow_RecordsOverflowWhenChannelIsFull(t *testing.T) { + ch := make(chan *buildTaggedTBTCSignerRoundContributionMessage, 1) + ch <- &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: 99} // fill it + rec := attempt.NewBoundedRecorder() + + payload := &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: 7} + if enqueueOrRecordOverflow(payload, ch, rec) { + t.Fatal("enqueue should fail when channel is full") + } + if got := rec.Snapshot().Overflows[7]; got != 1 { + t.Fatalf( + "overflow should be recorded against sender 7; got count %d", + got, + ) + } + if got := rec.Snapshot().Overflows[99]; got != 0 { + t.Fatal( + "sender 99 is the pre-filled payload's sender, not the overflow sender", + ) + } +} + +func TestEnqueueOrRecordOverflow_NoOpRecorderHasNoObservableEffect(t *testing.T) { + ch := make(chan *buildTaggedTBTCSignerRoundContributionMessage, 1) + ch <- &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: 1} + rec := attempt.NoOpRecorder() + + payload := &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: 7} + if enqueueOrRecordOverflow(payload, ch, rec) { + t.Fatal("enqueue should fail when channel is full") + } + if got := rec.Snapshot().Overflows[7]; got != 0 { + t.Fatalf( + "NoOp recorder must show zero overflow count even when called; got %d", + got, + ) + } +} + +func TestEnqueueOrRecordOverflow_RepeatedOverflowsSaturateAtQuota(t *testing.T) { + ch := make(chan *buildTaggedTBTCSignerRoundContributionMessage, 1) + ch <- &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: 1} + rec := attempt.NewBoundedRecorderWithQuota(3) + + for i := 0; i < 10; i++ { + _ = enqueueOrRecordOverflow( + &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: 2}, + ch, + rec, + ) + } + if got := rec.Snapshot().Overflows[2]; got != 3 { + t.Fatalf("expected saturation at quota 3, got %d", got) + } +} + +func TestEnqueueOrRecordOverflow_ConcurrentCallersAreRaceSafe(t *testing.T) { + const numProducers = 8 + const recordsPerProducer = 100 + ch := make(chan *buildTaggedTBTCSignerRoundContributionMessage, 1) + ch <- &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: 1} // fill it once + rec := attempt.NewBoundedRecorderWithQuota(uint(numProducers * recordsPerProducer)) + + var wg sync.WaitGroup + for p := 0; p < numProducers; p++ { + wg.Add(1) + sender := group.MemberIndex(p + 2) + go func() { + defer wg.Done() + for i := 0; i < recordsPerProducer; i++ { + _ = enqueueOrRecordOverflow( + &buildTaggedTBTCSignerRoundContributionMessage{SenderIDValue: uint32(sender)}, + ch, + rec, + ) + } + }() + } + wg.Wait() + + snap := rec.Snapshot() + totalRecorded := uint(0) + for _, v := range snap.Overflows { + totalRecorded += v + } + // Every producer's records either enqueued (replacing previously- + // dequeued items, but there's no consumer here so the channel stays + // full and all subsequent enqueue attempts fall to the default + // branch) or recorded. Since the channel starts pre-filled and has + // no consumer, all 800 records hit the overflow path. + const expected = numProducers * recordsPerProducer + if totalRecorded != expected { + t.Fatalf( + "concurrent overflow count: got %d, want %d (sum across senders)", + totalRecorded, expected, + ) + } +} diff --git a/pkg/frost/signing/legacy_backend.go b/pkg/frost/signing/legacy_backend.go new file mode 100644 index 0000000000..57b357ea83 --- /dev/null +++ b/pkg/frost/signing/legacy_backend.go @@ -0,0 +1,88 @@ +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 + } + + privateKeyShare, err := request.LegacyPrivateKeyShare() + if err != nil { + return nil, err + } + + 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 + } + + 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/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..e8864a619c --- /dev/null +++ b/pkg/frost/signing/native_adapter_build_frost_native_test.go @@ -0,0 +1,474 @@ +//go:build frost_native + +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 staticNativeBridgeProvider( + bridge NativeExecutionBridge, +) func() NativeExecutionBridge { + return func() NativeExecutionBridge { + return bridge + } +} + +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 { + t.Fatalf("unexpected native backend config error: [%v]", err) + } + + if CurrentExecutionBackendName() != NativeExecutionBackendName { + t.Fatalf( + "unexpected backend name\nexpected: [%s]\nactual: [%s]", + NativeExecutionBackendName, + CurrentExecutionBackendName(), + ) + } + + adapter := newBuildTaggedNativeExecutionAdapter() + + _, err = adapter.Execute(context.Background(), nil, nil) + if err == nil { + t.Fatal("expected request validation error") + } + + if !strings.Contains(err.Error(), "request is nil") { + t.Fatalf( + "unexpected native execution error\nexpected substring: [%s]\nactual: [%v]", + "request is nil", + err, + ) + } + + err = SetExecutionBackendByName("ffi") + if err != nil { + t.Fatalf( + "unexpected strict ffi backend config error\nexpected: [nil]\nactual: [%v]", + err, + ) + } + + UnregisterNativeExecutionBridge() + UnregisterNativeExecutionFFIExecutor() + + 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, + ) + } + + 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( + t *testing.T, +) { + expectedResult := &Result{} + bridge := &mockNativeExecutionBridge{ + available: true, + result: expectedResult, + } + + fallback := &mockExecutionBackend{name: "fallback"} + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridgeProvider: staticNativeBridgeProvider(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{ + nativeBridgeProvider: staticNativeBridgeProvider(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{ + nativeBridgeProvider: staticNativeBridgeProvider(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{ + nativeBridgeProvider: staticNativeBridgeProvider(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_Execute_StrictModeNoFallbackWhenUnavailable( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeStrict) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + bridge := &mockNativeExecutionBridge{ + available: false, + } + + fallback := &mockExecutionBackend{ + name: "fallback", + result: &Result{}, + } + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridgeProvider: staticNativeBridgeProvider(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{ + nativeBridgeProvider: staticNativeBridgeProvider(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, +) { + bridge := &mockNativeExecutionBridge{ + available: true, + } + + fallback := &mockExecutionBackend{name: "fallback"} + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridgeProvider: staticNativeBridgeProvider(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{ + nativeBridgeProvider: staticNativeBridgeProvider(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, + ) + } +} + +func TestBuildTaggedNativeExecutionAdapter_RegisterUnmarshallers_StrictModeNoFallback( + t *testing.T, +) { + setNativeExecutionMode(nativeExecutionModeStrict) + t.Cleanup(func() { + setNativeExecutionMode(nativeExecutionModeFallbackAllowed) + }) + + bridge := &mockNativeExecutionBridge{ + available: false, + } + + fallback := &mockExecutionBackend{name: "fallback"} + + adapter := &buildTaggedNativeExecutionAdapter{ + nativeBridgeProvider: staticNativeBridgeProvider(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.go b/pkg/frost/signing/native_adapter_registration.go new file mode 100644 index 0000000000..c4774da5a9 --- /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..313971394e --- /dev/null +++ b/pkg/frost/signing/native_adapter_registration_frost_native.go @@ -0,0 +1,142 @@ +//go:build frost_native + +package signing + +import ( + "context" + "errors" + "fmt" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" +) + +// buildTaggedNativeExecutionAdapter is a transitional adapter wired when the +// frost_native build tag is enabled. +// +// 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 { + nativeBridgeProvider func() NativeExecutionBridge + fallback ExecutionBackend +} + +func registerNativeExecutionAdapterForBuild() { + // Registration errors are surfaced via `LastNativeRegistrationError()` + // rather than panicking, so a transient registration failure at init time + // does not crash the binary. `currentNativeExecutionBackend()` already + // reports `ErrNativeCryptographyUnavailable` when no native adapter is + // registered, which keeps the legacy execution backend as the safe-by- + // default fallback. + err := RegisterNativeExecutionBridge(newBuildTaggedNativeExecutionBridge()) + if err != nil { + registrationLogger.Warnf( + "failed to register build-tagged native bridge: [%v]; "+ + "native execution will report unavailable and the legacy "+ + "execution backend remains the safe-by-default path", + err, + ) + setLastRegistrationError(fmt.Errorf( + "failed to register build-tagged native bridge: [%w]", + err, + )) + return + } + + err = RegisterNativeExecutionAdapter(newBuildTaggedNativeExecutionAdapter()) + if err != nil { + registrationLogger.Warnf( + "failed to register build-tagged native adapter: [%v]; "+ + "native execution will report unavailable and the legacy "+ + "execution backend remains the safe-by-default path", + err, + ) + setLastRegistrationError(fmt.Errorf( + "failed to register build-tagged native adapter: [%w]", + err, + )) + return + } +} + +func newBuildTaggedNativeExecutionAdapter() *buildTaggedNativeExecutionAdapter { + return &buildTaggedNativeExecutionAdapter{ + nativeBridgeProvider: newNativeExecutionBridge, + fallback: newLegacyExecutionBackend(), + } +} + +func (btnea *buildTaggedNativeExecutionAdapter) NativeExecutionAvailable() bool { + 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( + ctx context.Context, + logger log.StandardLogger, + request *Request, +) (*Result, error) { + nativeBridge := btnea.currentNativeBridge() + if nativeBridge != nil && nativeBridge.IsAvailable() { + result, err := 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 !nativeExecutionFallbackAllowed() { + return nil, err + } + + if logger != nil { + logger.Warnf( + "native FROST cryptography unavailable; falling back to legacy bridge backend: [%v]", + err, + ) + } + } + + if !nativeExecutionFallbackAllowed() { + return nil, ErrNativeCryptographyUnavailable + } + + 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, +) { + nativeBridge := btnea.currentNativeBridge() + if nativeBridge != nil && nativeBridge.IsAvailable() { + nativeBridge.RegisterUnmarshallers(channel) + return + } + + if !nativeExecutionFallbackAllowed() { + return + } + + if btnea.fallback == nil { + return + } + + btnea.fallback.RegisterUnmarshallers(channel) +} 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/frost/signing/native_bridge.go b/pkg/frost/signing/native_bridge.go new file mode 100644 index 0000000000..195369aed9 --- /dev/null +++ b/pkg/frost/signing/native_bridge.go @@ -0,0 +1,100 @@ +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", + ) + // 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 +// 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) +} + +// 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{} +} + +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, +) { +} 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..1cb5e9d186 --- /dev/null +++ b/pkg/frost/signing/native_bridge_frost_native.go @@ -0,0 +1,104 @@ +//go:build frost_native + +package signing + +import ( + "context" + "errors" + "fmt" + + "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 { + ffiExecutorProvider func() NativeExecutionFFIExecutor + delegate ExecutionBackend +} + +func newBuildTaggedNativeExecutionBridge() NativeExecutionBridge { + return &buildTaggedNativeExecutionBridge{ + ffiExecutorProvider: currentNativeExecutionFFIExecutor, + delegate: newLegacyExecutionBackend(), + } +} + +func (btneb *buildTaggedNativeExecutionBridge) IsAvailable() bool { + 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( + ctx context.Context, + 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 + } + + return btneb.delegate.Execute(ctx, logger, request) +} + +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 + } + + btneb.delegate.RegisterUnmarshallers(channel) +} 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..0608b743ac --- /dev/null +++ b/pkg/frost/signing/native_bridge_frost_native_test.go @@ -0,0 +1,365 @@ +//go:build frost_native + +package signing + +import ( + "context" + "errors" + "strings" + "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_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, +) { + 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/frost/signing/native_ffi_executor_adapter.go b/pkg/frost/signing/native_ffi_executor_adapter.go new file mode 100644 index 0000000000..1c6345a97a --- /dev/null +++ b/pkg/frost/signing/native_ffi_executor_adapter.go @@ -0,0 +1,152 @@ +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 + Channel net.BroadcastChannel + MembershipValidator *group.MembershipValidator + SignerMaterial *NativeSignerMaterial + TaprootMerkleRoot *[32]byte + 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, fmt.Errorf("%w: [%v]", ErrNativeCryptographyUnavailable, err) + } + + ffiRequest := &NativeExecutionFFISigningRequest{ + Message: request.Message, + SessionID: request.SessionID, + MemberIndex: request.MemberIndex, + GroupSize: request.GroupSize, + DishonestThreshold: request.DishonestThreshold, + Channel: request.Channel, + MembershipValidator: request.MembershipValidator, + SignerMaterial: signerMaterial, + TaprootMerkleRoot: cloneTaprootMerkleRoot(request.TaprootMerkleRoot), + Attempt: cloneAttempt(request.Attempt), + } + + // RFC-21 Phase 6.3: ROAST orchestration entry. The helper + // returns (cleanup, error): + // - cleanup non-nil, error nil -> orchestration active; + // defer cleanup so success and failure return paths converge. + // - cleanup nil, error nil -> static-configuration fallback + // (env var unset, no coordinator registered, or material + // format not extractable). Proceed without orchestration; the + // receive loops use NoOp recorder semantics (Phase 5 behaviour). + // - cleanup nil, error non-nil -> RUNTIME orchestration failure. + // HARD FAIL to prevent group fracture across honest signers. + // In the default build (no frost_native tag) the helper is a + // permanent no-op returning (nil, nil). + orchCleanup, orchErr := attemptRoastRetryOrchestrationFromRequest(ffiRequest, logger) + if orchErr != nil { + return nil, orchErr + } + if orchCleanup != nil { + defer orchCleanup() + } + + signature, err := nefea.primitive.Sign(ctx, logger, ffiRequest) + 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) +} + +func cloneTaprootMerkleRoot(taprootMerkleRoot *[32]byte) *[32]byte { + if taprootMerkleRoot == nil { + return nil + } + + result := new([32]byte) + copy(result[:], taprootMerkleRoot[:]) + + return result +} 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..565e5eaaf5 --- /dev/null +++ b/pkg/frost/signing/native_ffi_executor_adapter_test.go @@ -0,0 +1,316 @@ +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 !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]", + "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_ffi_primitive_registration.go b/pkg/frost/signing/native_ffi_primitive_registration.go new file mode 100644 index 0000000000..a62a38b19f --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration.go @@ -0,0 +1,107 @@ +package signing + +import ( + "fmt" + "sync" + + "github.com/ipfs/go-log/v2" +) + +var ( + registrationLogger = log.Logger("keep-frost-signing-registration") + protocolLogger = log.Logger("keep-frost-signing-protocol") + registrationErrorMu sync.RWMutex + lastRegistrationError error +) + +func setLastRegistrationError(err error) { + registrationErrorMu.Lock() + defer registrationErrorMu.Unlock() + lastRegistrationError = err +} + +// LastNativeRegistrationError returns the most recent error observed while +// registering build-tagged native FROST execution adapters or FFI signing +// primitives. It is nil when the most recent registration attempt succeeded +// or when no registration has been attempted yet. Callers that want to fail +// startup on a registration error should check this after invoking +// `RegisterNativeExecutionAdapterForBuild` rather than relying on the +// previously panicking registration helpers themselves. +func LastNativeRegistrationError() error { + registrationErrorMu.RLock() + defer registrationErrorMu.RUnlock() + return lastRegistrationError +} + +// 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. +// +// Registration errors are surfaced via `LastNativeRegistrationError()` rather +// than panicking, so a transient FFI lookup failure at init time does not +// crash the binary. Downstream code in `pkg/frost/signing/backend.go` already +// handles the absence of a registered native adapter through +// `ErrNativeCryptographyUnavailable`, so the legacy execution backend remains +// the safe-by-default path even when this registration fails. +func RegisterNativeExecutionFFISigningPrimitiveForBuild() { + err := registerNativeExecutionFFISigningPrimitiveForBuild() + if err != nil { + registrationLogger.Warnf( + "failed to register build-tagged native FFI signing primitive: [%v]; "+ + "the native execution backend will report unavailable and callers "+ + "that selected the legacy or native-with-fallback backend will "+ + "continue using the legacy bridge", + err, + ) + setLastRegistrationError(fmt.Errorf( + "failed to register build-tagged native FFI signing primitive: [%w]", + err, + )) + return + } + + setLastRegistrationError(nil) +} 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..a68007ea45 --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_default.go @@ -0,0 +1,7 @@ +//go:build !frost_native + +package signing + +func registerNativeExecutionFFISigningPrimitiveForBuild() error { + return 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 new file mode 100644 index 0000000000..d6d3b3b3c8 --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_frost_native.go @@ -0,0 +1,23 @@ +//go:build frost_native + +package signing + +import "fmt" + +func registerNativeExecutionFFISigningPrimitiveForBuild() error { + provider := currentNativeExecutionFFISigningPrimitiveProviderForBuild() + if provider == nil { + provider = defaultNativeExecutionFFISigningPrimitiveProviderForBuild + } + + 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..16d9468b2e --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_frost_native_test.go @@ -0,0 +1,106 @@ +//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_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_ProviderErrorIsRecordedNotPanicked( + t *testing.T, +) { + UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild() + UnregisterNativeExecutionFFIExecutor() + t.Cleanup(UnregisterNativeExecutionFFISigningPrimitiveProviderForBuild) + t.Cleanup(UnregisterNativeExecutionFFIExecutor) + t.Cleanup(func() { setLastRegistrationError(nil) }) + + 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() { + if recovered := recover(); recovered != nil { + t.Fatalf( + "registration must not panic; recovered: [%v]", + recovered, + ) + } + }() + + // Pre-condition: the registration error slot is clear before invoking the + // helper, so any non-nil error after the call is from this attempt. + setLastRegistrationError(nil) + + RegisterNativeExecutionFFISigningPrimitiveForBuild() + + registered := LastNativeRegistrationError() + if registered == nil { + t.Fatal("expected LastNativeRegistrationError to surface the provider error") + } + if !strings.Contains(registered.Error(), expectedErr.Error()) { + t.Fatalf( + "LastNativeRegistrationError missing expected substring\nexpected: [%s]\nactual: [%v]", + expectedErr.Error(), + registered, + ) + } + + if currentNativeExecutionFFIExecutor() != nil { + t.Fatal( + "FFI executor must not be registered when the provider returned an error", + ) + } +} 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..6711b0b105 --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_registration_test.go @@ -0,0 +1,26 @@ +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, + ) + } +} 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..a031201c61 --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native.go @@ -0,0 +1,1572 @@ +//go:build frost_native + +package signing + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "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, +) { + if err := registerBuildTaggedNativeFROSTSigningEngine(); err != nil { + return nil, err + } + + return &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{}, nil +} + +// buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive is a +// transitional primitive that preserves legacy bridge 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. Unsupported +// `frost-uniffi-v2` material is rejected explicitly because it cannot produce +// Taproot-tweaked signatures; accepting it would allow new deposits to a +// wallet that cannot sweep them. +type buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive struct{} + +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" + +// buildTaggedTBTCSignerConsumedAttemptReplayErrorCode is the structured Rust +// `ErrorResponse.code` value emitted by tbtc-signer when an `attempt_id` is +// reused after consumption. Preferred over substring matching on the message +// because the code is contract-stable: see `EngineError::code()` in the +// `tbtc-signer` crate. +const buildTaggedTBTCSignerConsumedAttemptReplayErrorCode = "consumed_attempt_replay" + +// buildTaggedTBTCSignerLegacyValidationErrorCode is the structured code +// emitted by tbtc-signer builds that pre-date the dedicated replay variant. +// Those builds route the replay path through `EngineError::Validation`, so +// the code on the wire is `validation_error` and the substring check on the +// message is the only signal callers have. Once the rolling upgrade is past +// the minimum-supported signer version, this code can be retired. +const buildTaggedTBTCSignerLegacyValidationErrorCode = "validation_error" + +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 nativeTBTCSignerVersionedEngine interface { + Version() (string, error) +} + +type buildTaggedTBTCSignerRoundContributionMessage struct { + SenderIDValue uint32 `json:"senderID"` + SessionIDValue string `json:"sessionID"` + ContributionIdentifier uint16 `json:"contributionIdentifier"` + ContributionData []byte `json:"contributionData"` + // AttemptContextHash binds this contribution to an RFC-21 attempt + // context when ROAST retry has registered one for the session. + AttemptContextHash []byte `json:"attemptContextHash,omitempty"` +} + +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") + } + + if err := validateAttemptContextHashField( + bttsrcm.AttemptContextHash, + ); err != nil { + return err + } + + return nil +} + +func (bttsrcm *buildTaggedTBTCSignerRoundContributionMessage) SetAttemptContextHash( + hash [AttemptContextHashFieldLength]byte, +) { + bttsrcm.AttemptContextHash = attemptContextHashFieldFromArray(hash) +} + +func (bttsrcm *buildTaggedTBTCSignerRoundContributionMessage) GetAttemptContextHash() ( + [AttemptContextHashFieldLength]byte, bool, +) { + return attemptContextHashFieldToArray(bttsrcm.AttemptContextHash) +} + +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") + } + + if request.SignerMaterial == nil { + return nil, fmt.Errorf( + "%w: signer material is nil", + ErrNativeCryptographyUnavailable, + ) + } + + switch request.SignerMaterial.Format { + case NativeSignerMaterialFormatFrostUniFFIV2: + return nil, fmt.Errorf( + "%w: unsupported UniFFI FROST signer material format [%s]; it cannot sweep Taproot deposits; use [%s]", + ErrUnsupportedSignerMaterialFormat, + NativeSignerMaterialFormatFrostUniFFIV2, + NativeSignerMaterialFormatFrostTBTCSignerV1, + ) + + 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]", + ErrNativeCryptographyUnavailable, + request.SignerMaterial.Format, + ) + } +} + +func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithTBTCSignerCoarseEngine( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + payload, err := decodeBuildTaggedTBTCSignerMaterialPayload(request.SignerMaterial) + if err != nil { + return nil, err + } + + // Scaffold persistence-vs-execution gate. The resolver in #3959 refuses to + // BUILD scaffold-era signer material without the env opt-in, but material + // persisted from a previous opted-in session can still drive this signing + // path on later runs after the operator has unset the flag. Refuse to + // enter the FFI scaffold path (which feeds placeholder participant + // pubkeys into RunDKG) when the payload is scaffold-era and the operator + // has not actively opted in for this process. The check is per-call (not + // cached) so flipping the env back unset recovers fail-closed behavior + // without a restart, matching the contract documented on + // AcceptScaffoldKeyGroupEnvVar. + if payload.KeyGroupSource == NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey && + !AcceptScaffoldKeyGroupEnabled() { + return nil, fmt.Errorf( + "%w: refusing to drive the tbtc-signer FFI signing path with "+ + "scaffold-era %q signer material; set %s=true to opt in for "+ + "local/CI use only, never in production", + ErrNativeCryptographyUnavailable, + NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + AcceptScaffoldKeyGroupEnvVar, + ) + } + + legacyPrivateKeyShare, err := decodeBuildTaggedTBTCSignerLegacyPrivateKeyShare(payload) + if err != nil { + return nil, err + } + + nativeEngine := currentNativeTBTCSignerEngine() + if nativeEngine == nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + "native tbtc-signer engine is unavailable", + payload.KeyGroupSource, + ) + } + + includedMembersSet, includedMembersIndexes, err := includedMembersFromRequest(request) + if err != nil { + if errors.Is(err, ErrInvalidSigningAttemptPolicy) { + return nil, fmt.Errorf( + "%w: invalid tbtc-signer signing attempt policy: %w", + ErrNativeBridgeOperationFailed, + err, + ) + } + + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf("cannot determine included members: [%v]", err), + payload.KeyGroupSource, + ) + } + + dkgParticipants, dkgThreshold, err := buildTaggedTBTCSignerRunDKGInputsForPayload( + payload, + request, + includedMembersIndexes, + ) + if err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf("cannot prepare tbtc-signer RunDKG request: [%v]", err), + payload.KeyGroupSource, + ) + } + + dkgResult, err := runNativeTBTCSignerDKG( + nativeEngine, + request.SessionID, + dkgParticipants, + dkgThreshold, + payload.DKGSeedHex, + ) + if err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf("tbtc-signer RunDKG failed: [%v]", err), + 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, + ) + } + + keyGroupForRound, keyGroupSubstituted, err := buildTaggedTBTCSignerRoundKeyGroup( + payload, + dkgResult, + ) + if err != nil { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + err.Error(), + payload.KeyGroupSource, + ) + } + + 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( + 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, + fmt.Sprintf( + "cannot query tbtc-signer version; coarse round scaffold skipped: [%v]", + err, + ), + payload.KeyGroupSource, + ) + } + + if !isBuildTaggedTBTCSignerBootstrapVersion(engineVersion) { + return btlcnnefsp.fallbackTBTCSignerLegacySigning( + ctx, + logger, + request, + legacyPrivateKeyShare, + fmt.Sprintf( + "tbtc-signer version [%s] is not bootstrap; coarse round scaffold skipped", + engineVersion, + ), + payload.KeyGroupSource, + ) + } + + coarseSignatureBytes, err := executeBuildTaggedTBTCSignerBootstrapCoarseRoundWithSignature( + ctx, + request, + keyGroupForRound, + nativeEngine, + includedMembersSet, + 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, + request, + legacyPrivateKeyShare, + fmt.Sprintf("tbtc-signer bootstrap coarse round failed: [%v]", err), + payload.KeyGroupSource, + ) + } + + 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; returning coarse signature", + ) + } + + emitNativeTBTCSignerCoarseSignatureEvent( + NativeTBTCSignerCoarseSignatureEvent{ + SessionID: request.SessionID, + KeyGroupSource: payload.KeyGroupSource, + EngineVersion: engineVersion, + }, + ) + + return coarseSignature, nil +} + +func isBuildTaggedTBTCSignerConsumedAttemptReplayError(err error) bool { + if err == nil { + return false + } + + // Prefer the structured `code` field from the FFI error envelope when it + // is reachable through the error chain. The Rust signer's + // `EngineError::code()` value `"consumed_attempt_replay"` is a + // contract-stable identifier; this check survives any cosmetic rewording + // of the human-readable message on either side. + // + // Older signer builds emit `validation_error` for the replay path with + // the legacy wording in the message. For those, fall through to the + // substring check restricted to the structured message field so a + // `validation_error` carrying an unrelated error chain string cannot be + // mistaken for a replay. Any other recognized code is authoritative. + var structured *buildTaggedTBTCSignerStructuredError + if errors.As(err, &structured) && structured.Code != "" { + switch structured.Code { + case buildTaggedTBTCSignerConsumedAttemptReplayErrorCode: + return true + case buildTaggedTBTCSignerLegacyValidationErrorCode: + return messageMatchesLegacyConsumedAttemptReplay(structured.Message) + default: + return false + } + } + + // No structured code reachable — the error chain pre-dates the FFI + // envelope. The legacy wording is preserved by the current tbtc-signer + // release so this branch continues to work during the rolling upgrade + // window. Match on the whole rendered string for maximum compatibility. + return messageMatchesLegacyConsumedAttemptReplay(err.Error()) +} + +func messageMatchesLegacyConsumedAttemptReplay(message string) bool { + lower := strings.ToLower(message) + return strings.Contains(lower, "attempt_id") && + strings.Contains(lower, buildTaggedTBTCSignerConsumedAttemptReplayErrorFragment) +} + +func buildTaggedTBTCSignerRunDKGInputs( + request *NativeExecutionFFISigningRequest, +) ([]NativeTBTCSignerDKGParticipant, uint16, error) { + _, includedMembersIndexes, err := includedMembersFromRequest(request) + if err != nil { + return nil, 0, err + } + + return buildTaggedTBTCSignerRunDKGInputsForIncludedMembers( + request, + includedMembersIndexes, + ) +} + +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") + } + + attempt := request.Attempt + if attempt != nil { + if attempt.Number == 0 { + return nil, nil, fmt.Errorf( + "%w: attempt number is zero", + ErrInvalidSigningAttemptPolicy, + ) + } + + if attempt.CoordinatorMemberIndex == 0 { + return nil, nil, fmt.Errorf( + "%w: attempt coordinator member index is zero", + ErrInvalidSigningAttemptPolicy, + ) + } + } + + 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 attempt != nil && len(attempt.IncludedMembersIndexes) > 0 { + for _, memberIndex := range attempt.IncludedMembersIndexes { + if memberIndex == 0 { + return nil, nil, fmt.Errorf( + "%w: included member index is zero", + ErrInvalidSigningAttemptPolicy, + ) + } + + if _, excluded := excludedMembersSet[memberIndex]; excluded { + return nil, nil, fmt.Errorf( + "%w: member [%v] is both included and excluded in attempt", + ErrInvalidSigningAttemptPolicy, + memberIndex, + ) + } + + includedMembersSet[memberIndex] = struct{}{} + } + } else { + for i := 1; i <= request.GroupSize; i++ { + memberIndex := group.MemberIndex(i) + if _, excluded := excludedMembersSet[memberIndex]; !excluded { + includedMembersSet[memberIndex] = struct{}{} + } + } + } + + 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( + "%w: attempt coordinator [%v] is not included", + ErrInvalidSigningAttemptPolicy, + attempt.CoordinatorMemberIndex, + ) + } + } + + 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 buildTaggedTBTCSignerRunDKGInputsForPayload( + payload *NativeTBTCSignerMaterialPayload, + request *NativeExecutionFFISigningRequest, + includedMembersIndexes []group.MemberIndex, +) ([]NativeTBTCSignerDKGParticipant, uint16, error) { + if payload != nil && + payload.KeyGroupSource == NativeTBTCSignerKeyGroupSourceDKGPersisted { + if len(payload.DKGParticipants) < 2 { + return nil, 0, fmt.Errorf( + "persisted tbtc-signer DKG participants are insufficient", + ) + } + if payload.DKGThreshold == 0 { + return nil, 0, fmt.Errorf( + "persisted tbtc-signer DKG threshold is zero", + ) + } + if int(payload.DKGThreshold) > len(payload.DKGParticipants) { + return nil, 0, fmt.Errorf( + "persisted tbtc-signer DKG threshold exceeds participant count: [%v] > [%v]", + payload.DKGThreshold, + len(payload.DKGParticipants), + ) + } + + participants := make( + []NativeTBTCSignerDKGParticipant, + len(payload.DKGParticipants), + ) + copy(participants, payload.DKGParticipants) + + return participants, payload.DKGThreshold, nil + } + + 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") + } + + 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) +} + +// NativeTBTCSignerDKGPlaceholderPublicKeyHex returns the transitional +// placeholder public key used by tbtc-signer dealer-DKG requests. +func NativeTBTCSignerDKGPlaceholderPublicKeyHex(identifier uint16) string { + return buildTaggedTBTCSignerDKGPlaceholderPublicKeyHex(identifier) +} + +func buildTaggedTBTCSignerRoundKeyGroup( + payload *NativeTBTCSignerMaterialPayload, + dkgResult *NativeTBTCSignerDKGResult, +) (string, bool, error) { + if payload == nil { + return "", false, fmt.Errorf("tbtc-signer payload is nil") + } + + if dkgResult == nil { + return "", false, fmt.Errorf("tbtc-signer RunDKG result is nil") + } + + if dkgResult.KeyGroup == "" { + return "", false, fmt.Errorf("tbtc-signer RunDKG key group is empty") + } + + if payload.KeyGroup == dkgResult.KeyGroup { + 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. + // Refuse the substitution by default so a production deployment that + // somehow ended up with placeholder material does not silently route + // signing through whatever key group the Rust side happens to return. + // The operator must explicitly opt into the scaffold path via + // AcceptScaffoldKeyGroupEnvVar; the env-var check is per-call (not + // cached) so flipping it off recovers fail-closed behavior without a + // restart. + if !AcceptScaffoldKeyGroupEnabled() { + return "", false, fmt.Errorf( + "tbtc-signer key group source %q is scaffold-era placeholder "+ + "material and may not be silently substituted with the "+ + "RunDKG output; set %s=true to opt in for local/CI use "+ + "only, never in production", + NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + AcceptScaffoldKeyGroupEnvVar, + ) + } + 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 true +} + +func executeBuildTaggedTBTCSignerBootstrapCoarseRound( + ctx context.Context, + request *NativeExecutionFFISigningRequest, + keyGroup string, + nativeEngine NativeTBTCSignerEngine, + 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 nil, fmt.Errorf("request is nil") + } + + if request.Message == nil { + return nil, fmt.Errorf("request message is nil") + } + + if nativeEngine == nil { + return nil, fmt.Errorf("native tbtc-signer engine is nil") + } + + if ctx == nil { + ctx = context.Background() + } + + if includedMembersSet == nil || len(includedMembersIndexes) == 0 { + var err error + includedMembersSet, includedMembersIndexes, err = includedMembersFromRequest(request) + if err != nil { + return nil, fmt.Errorf("cannot determine included members: [%w]", err) + } + } + + if _, ok := includedMembersSet[request.MemberIndex]; !ok { + return nil, fmt.Errorf( + "member [%v] not included in tbtc-signer signing attempt", + request.MemberIndex, + ) + } + + messageDigest, err := messageDigestFromBigInt(request.Message) + if err != nil { + return nil, fmt.Errorf("invalid request message digest: [%v]", err) + } + messageBytes := make([]byte, len(messageDigest)) + copy(messageBytes, messageDigest[:]) + + if request.MemberIndex == 0 { + return nil, fmt.Errorf("request member index is zero") + } + + signingParticipants, err := buildTaggedTBTCSignerSigningParticipants( + includedMembersIndexes, + ) + if err != nil { + return nil, fmt.Errorf("cannot derive signing participants: [%w]", err) + } + + roundState, err := nativeEngine.StartSignRound( + request.SessionID, + uint16(request.MemberIndex), + messageBytes, + keyGroup, + signingParticipants, + request.TaprootMerkleRoot, + ) + if err != nil { + return nil, fmt.Errorf("start sign round failed: [%w]", err) + } + + if roundState == nil { + return nil, fmt.Errorf("start sign round returned nil state") + } + + if roundState.RequiredContributions == 0 { + return nil, fmt.Errorf("start sign round required contributions are zero") + } + + if len(signingParticipants) > 0 { + if len(roundState.SigningParticipants) != len(signingParticipants) { + return nil, 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 nil, 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, + roundState, + includedMembersSet, + includedMembersIndexes, + ) + if err != nil { + return nil, fmt.Errorf("cannot collect round contributions: [%w]", err) + } + + if len(roundContributions) < int(roundState.RequiredContributions) { + return nil, fmt.Errorf( + "insufficient round contributions: [%v] < [%v]", + len(roundContributions), + roundState.RequiredContributions, + ) + } + + signature, err := nativeEngine.FinalizeSignRound( + request.SessionID, + roundContributions, + request.TaprootMerkleRoot, + ) + if err != nil { + return nil, fmt.Errorf("finalize sign round failed: [%w]", err) + } + + if len(signature) == 0 { + return nil, fmt.Errorf("finalize sign round returned empty signature") + } + + return signature, nil +} + +func decodeBuildTaggedTBTCSignerSignature(signature []byte) (*frost.Signature, error) { + if len(signature) == 0 { + return nil, fmt.Errorf("signature is empty") + } + + // Unmarshal validates length and splits the wire value into R/S. The + // tbtc-signer material carries a key-group handle rather than the x-only + // output key, so this layer can only enforce canonical Schnorr encoding. + // Key-bound verification happens downstream when the wallet output key is + // available. + result := &frost.Signature{} + if err := result.Unmarshal(signature); err != nil { + return nil, fmt.Errorf("invalid frost signature bytes: [%w]", err) + } + + serialized := result.Serialize() + if _, err := schnorr.ParseSignature(serialized[:]); err != nil { + return nil, fmt.Errorf("non-canonical BIP-340 signature bytes: [%w]", err) + } + + return result, 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, + 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, + ) + } + + ownContribution, err := buildTaggedTBTCSignerOwnRoundContribution( + request, + roundState, + ) + if err != nil { + return nil, fmt.Errorf("cannot build own round contribution: [%w]", err) + } + + roundContributionMessage := &buildTaggedTBTCSignerRoundContributionMessage{ + SenderIDValue: uint32(request.MemberIndex), + SessionIDValue: request.SessionID, + ContributionIdentifier: ownContribution.Identifier, + ContributionData: append([]byte{}, ownContribution.Data...), + } + setMessageAttemptContextHashIfBound(roundContributionMessage, request.SessionID) + + if err := request.Channel.Send( + ctx, + roundContributionMessage, + net.BackoffRetransmissionStrategy, + ); err != nil { + return nil, fmt.Errorf("cannot send round contribution message: [%w]", err) + } + + // RFC-21 Phase 4.2/4.3: recorder comes from the roast-retry + // registry; deferred submission pushes the snapshot into + // Coordinator.RecordEvidence at end-of-collect. NoOp fallback + // when nothing is registered preserves Phase 2 receive + // semantics. + contributionsRecorder := roastRetryRecorderForCollect() + defer submitSnapshotIfActive(request.SessionID, contributionsRecorder) + peerMessages, err := collectBuildTaggedTBTCSignerRoundContributionMessages( + ctx, + request, + includedMembersSet, + includedMembersIndexes, + contributionsRecorder, + ) + 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 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, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, + evidence attempt.EvidenceRecorder, +) (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(), + ) { + evidence.RecordReject(payload.SenderID(), "validation_gate_rejected") + return + } + + if err := verifyMessageAttemptContextHash(payload, request.SessionID); err != nil { + evidence.RecordReject(payload.SenderID(), "attempt_context_hash_mismatch") + return + } + + _ = enqueueOrRecordOverflow(payload, messageChan, evidence) + }) + + 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: + // First-write-wins / equal-or-reject. A peer that retransmits the + // same contribution is idempotent; a peer that mutates its own + // contribution after the first send is a ROAST evidence concern + // and must not be allowed to overwrite the persisted view. + senderID := message.SenderID() + if existing, ok := receivedMessages[senderID]; ok { + if !buildTaggedTBTCSignerRoundContributionMessagesEqual( + existing, + message, + ) { + evidence.RecordConflict(senderID) + protocolLogger.Warnf( + "dropping conflicting tbtc-signer round contribution "+ + "from sender [%d]; first-write-wins keeps the "+ + "originally accepted contribution", + senderID, + ) + } + continue + } + receivedMessages[senderID] = message + } + } + + return receivedMessages, nil +} + +func buildTaggedTBTCSignerRoundContributionMessagesEqual( + left, right *buildTaggedTBTCSignerRoundContributionMessage, +) bool { + if left == nil || right == nil { + return left == right + } + return left.SenderIDValue == right.SenderIDValue && + left.SessionIDValue == right.SessionIDValue && + left.ContributionIdentifier == right.ContributionIdentifier && + bytes.Equal(left.ContributionData, right.ContributionData) && + bytes.Equal(left.AttemptContextHash, right.AttemptContextHash) +} + +func buildTaggedTBTCSignerSyntheticRoundContributions( + roundState *NativeTBTCSignerRoundState, + includedMembersIndexes []group.MemberIndex, +) ([]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, + len(includedMembersIndexes), + ) + + for _, memberIndex := range includedMembersIndexes { + if memberIndex == 0 { + 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: append([]byte{}, shareDigest[:]...), + }, + ) + } + + return contributions, nil +} + +func (btlcnnefsp *buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive) signWithLegacyTECDSABridge( + ctx context.Context, + logger log.StandardLogger, + request *NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + privateKeyShare, err := decodeBuildTaggedLegacyPrivateKeyShare( + request.SignerMaterial, + ) + if err != nil { + 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 request.TaprootMerkleRoot != nil { + return nil, fmt.Errorf( + "%w: taproot tweaked signing requires native FROST signer support", + ErrNativeCryptographyUnavailable, + ) + } + + if privateKeyShare == nil { + return nil, fmt.Errorf("legacy private key share is nil") + } + + 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) fallbackTBTCSignerLegacySigning( + ctx context.Context, + logger log.StandardLogger, + 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) + } + + 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, +) { + registerBuildTaggedTBTCSignerUnmarshallers(channel) + legacySigning.RegisterUnmarshallers(channel) +} + +func registerBuildTaggedTBTCSignerUnmarshallers(channel net.BroadcastChannel) { + channel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &buildTaggedTBTCSignerRoundContributionMessage{} + }) +} + +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 +} + +func decodeBuildTaggedTBTCSignerMaterialPayload( + signerMaterial *NativeSignerMaterial, +) (*NativeTBTCSignerMaterialPayload, error) { + if signerMaterial == nil { + return nil, fmt.Errorf( + "%w: signer material is nil", + ErrNativeCryptographyUnavailable, + ) + } + + if signerMaterial.Format != NativeSignerMaterialFormatFrostTBTCSignerV1 { + 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 payload NativeTBTCSignerMaterialPayload + if err := json.Unmarshal(signerMaterial.Payload, &payload); err != nil { + return nil, fmt.Errorf( + "%w: cannot unmarshal tbtc-signer payload: [%v]", + ErrNativeCryptographyUnavailable, + err, + ) + } + + if payload.KeyGroup == "" { + 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 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) +} + +func decodeBuildTaggedTBTCSignerLegacyPrivateKeyShare( + payload *NativeTBTCSignerMaterialPayload, +) (*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 +} + +func runNativeTBTCSignerDKG( + nativeEngine NativeTBTCSignerEngine, + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + dkgSeedHex string, +) (*NativeTBTCSignerDKGResult, error) { + if dkgSeedHex == "" { + return nativeEngine.RunDKG(sessionID, participants, threshold) + } + + seededEngine, ok := nativeEngine.(NativeTBTCSignerSeededDKGEngine) + if !ok { + return nil, fmt.Errorf( + "native tbtc-signer engine does not support seeded RunDKG", + ) + } + + return seededEngine.RunDKGWithSeed( + sessionID, + participants, + threshold, + dkgSeedHex, + ) +} 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..8b78d39f5d --- /dev/null +++ b/pkg/frost/signing/native_ffi_primitive_transitional_frost_native_test.go @@ -0,0 +1,3141 @@ +//go:build frost_native + +package signing + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "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" +) + +type mockBuildTaggedTBTCSignerEngine struct { + runDKGCalled bool + runDKGSessionID string + runDKGParticipants []NativeTBTCSignerDKGParticipant + runDKGThreshold uint16 + runDKGResult *NativeTBTCSignerDKGResult + runDKGErr error + runDKGFn func( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + ) (*NativeTBTCSignerDKGResult, error) + version string + versionErr error + startCalled bool + startSessionID string + startMemberID uint16 + startMessage []byte + startKeyGroup string + startSigningParticipants []uint16 + startSignRoundFn func( + sessionID string, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, + taprootMerkleRoot *[32]byte, + ) (*NativeTBTCSignerRoundState, error) + startTaprootMerkleRoot *[32]byte + startRoundState *NativeTBTCSignerRoundState + startErr error + finalizeCalled bool + finalizeSessionID string + finalizeTaprootMerkleRoot *[32]byte + finalizeInputs []NativeTBTCSignerRoundContribution + finalizeSignature []byte + finalizeErr error +} + +func (mbttse *mockBuildTaggedTBTCSignerEngine) RunDKG( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, +) (*NativeTBTCSignerDKGResult, error) { + mbttse.runDKGCalled = true + mbttse.runDKGSessionID = sessionID + mbttse.runDKGParticipants = append( + []NativeTBTCSignerDKGParticipant{}, + participants..., + ) + mbttse.runDKGThreshold = threshold + + if mbttse.runDKGErr != nil { + return nil, mbttse.runDKGErr + } + + if mbttse.runDKGFn != nil { + return mbttse.runDKGFn(sessionID, participants, threshold) + } + + 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) Version() (string, error) { + if mbttse.versionErr != nil { + return "", mbttse.versionErr + } + + return mbttse.version, nil +} + +func (mbttse *mockBuildTaggedTBTCSignerEngine) StartSignRound( + sessionID string, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, + taprootMerkleRoot *[32]byte, +) (*NativeTBTCSignerRoundState, error) { + mbttse.startCalled = true + mbttse.startSessionID = sessionID + mbttse.startMemberID = memberIdentifier + mbttse.startMessage = append([]byte{}, message...) + mbttse.startKeyGroup = keyGroup + mbttse.startSigningParticipants = append([]uint16{}, signingParticipants...) + mbttse.startTaprootMerkleRoot = cloneTestTaprootMerkleRoot(taprootMerkleRoot) + + if mbttse.startErr != nil { + return nil, mbttse.startErr + } + + if mbttse.startSignRoundFn != nil { + return mbttse.startSignRoundFn( + sessionID, + memberIdentifier, + message, + keyGroup, + signingParticipants, + taprootMerkleRoot, + ) + } + + if mbttse.startRoundState != nil { + if len(mbttse.startRoundState.SigningParticipants) == 0 { + mbttse.startRoundState.SigningParticipants = append( + []uint16{}, + signingParticipants..., + ) + } + + return mbttse.startRoundState, nil + } + + return &NativeTBTCSignerRoundState{ + SessionID: sessionID, + RoundID: "round-1", + RequiredContributions: 2, + MessageDigestHex: "00", + SigningParticipants: append([]uint16{}, signingParticipants...), + }, nil +} + +func (mbttse *mockBuildTaggedTBTCSignerEngine) FinalizeSignRound( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, + taprootMerkleRoot *[32]byte, +) ([]byte, error) { + mbttse.finalizeCalled = true + mbttse.finalizeSessionID = sessionID + mbttse.finalizeTaprootMerkleRoot = cloneTestTaprootMerkleRoot(taprootMerkleRoot) + 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 (mbttse *mockBuildTaggedTBTCSignerEngine) BuildTaprootTx( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, +) (*NativeTBTCSignerTxResult, error) { + return nil, errors.New("not implemented") +} + +func cloneTestTaprootMerkleRoot(taprootMerkleRoot *[32]byte) *[32]byte { + if taprootMerkleRoot == nil { + return nil + } + + result := new([32]byte) + copy(result[:], taprootMerkleRoot[:]) + + return result +} + +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, + memberIdentifier uint16, + _ []byte, + _ string, + signingParticipants []uint16, + _ *[32]byte, +) (*NativeTBTCSignerRoundState, error) { + if dbttsbre.roundState != nil { + if dbttsbre.roundState.OwnContribution == nil { + dbttsbre.roundState.OwnContribution = &NativeTBTCSignerRoundContribution{ + Identifier: memberIdentifier, + Data: []byte{byte(memberIdentifier), 0xab}, + } + } + + if len(dbttsbre.roundState.SigningParticipants) == 0 { + dbttsbre.roundState.SigningParticipants = append( + []uint16{}, + signingParticipants..., + ) + } + + 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}, + }, + }, nil +} + +func (dbttsbre *deterministicBuildTaggedTBTCSignerBootstrapRoundEngine) FinalizeSignRound( + _ string, + roundContributions []NativeTBTCSignerRoundContribution, + _ *[32]byte, +) ([]byte, error) { + dbttsbre.finalizeMutex.Lock() + defer dbttsbre.finalizeMutex.Unlock() + + dbttsbre.finalizeCalls++ + dbttsbre.finalizeInput = append( + []NativeTBTCSignerRoundContribution{}, + roundContributions..., + ) + + 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() + + 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 TestDecodeBuildTaggedTBTCSignerSignatureRejectsNonCanonicalBIP340( + t *testing.T, +) { + _, err := decodeBuildTaggedTBTCSignerSignature(bytes.Repeat([]byte{0xff}, 64)) + if err == nil { + t.Fatal("expected non-canonical BIP-340 signature bytes to be rejected") + } + if !strings.Contains(err.Error(), "non-canonical BIP-340 signature bytes") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +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 TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_RejectsUnsupportedUniFFIV2Material( + t *testing.T, +) { + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + _, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV2, + Payload: []byte{0x01}, + }, + }) + if err == nil { + t.Fatal("expected unsupported material error") + } + if !errors.Is(err, ErrUnsupportedSignerMaterialFormat) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrUnsupportedSignerMaterialFormat, + err, + ) + } + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "unsupported signer material should not be reported as unavailable native cryptography: [%v]", + 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, + ) + } + }) + } +} + +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 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( + &NativeTBTCSignerMaterialPayload{ + 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 *NativeTBTCSignerMaterialPayload + expectError bool + }{ + { + name: "nil payload", + payload: nil, + expectError: false, + }, + { + name: "empty legacy private key share", + payload: &NativeTBTCSignerMaterialPayload{}, + expectError: false, + }, + { + name: "invalid hex", + payload: &NativeTBTCSignerMaterialPayload{ + LegacyPrivateKeyShareHex: "zz", + }, + expectError: true, + }, + { + name: "invalid private key share payload", + payload: &NativeTBTCSignerMaterialPayload{ + 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 TestBuildTaggedTBTCSignerRunDKGInputs(t *testing.T) { + participants, threshold, err := buildTaggedTBTCSignerRunDKGInputs( + &NativeExecutionFFISigningRequest{ + GroupSize: 5, + DishonestThreshold: 2, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + 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 TestBuildTaggedTBTCSignerRunDKGInputsForPayload_UsesPersistedDKGInputs( + t *testing.T, +) { + persistedParticipants := []NativeTBTCSignerDKGParticipant{ + {Identifier: 1, PublicKeyHex: "020001"}, + {Identifier: 2, PublicKeyHex: "020002"}, + {Identifier: 3, PublicKeyHex: "020003"}, + } + + participants, threshold, err := buildTaggedTBTCSignerRunDKGInputsForPayload( + &NativeTBTCSignerMaterialPayload{ + KeyGroupSource: NativeTBTCSignerKeyGroupSourceDKGPersisted, + DKGParticipants: persistedParticipants, + DKGThreshold: 2, + }, + &NativeExecutionFFISigningRequest{ + GroupSize: 3, + DishonestThreshold: 1, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 3}, + }, + }, + []group.MemberIndex{1, 3}, + ) + if err != nil { + t.Fatalf("unexpected RunDKG inputs error: [%v]", err) + } + + if threshold != 2 { + t.Fatalf("unexpected threshold: [%v]", threshold) + } + if len(participants) != len(persistedParticipants) { + t.Fatalf( + "unexpected participants count\nexpected: [%v]\nactual: [%v]", + len(persistedParticipants), + len(participants), + ) + } + for i := range participants { + if participants[i] != persistedParticipants[i] { + t.Fatalf( + "unexpected participant at index [%d]\nexpected: [%+v]\nactual: [%+v]", + i, + persistedParticipants[i], + participants[i], + ) + } + } +} + +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 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", + 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 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) + + engineByMember := map[group.MemberIndex]*deterministicBuildTaggedTBTCSignerBootstrapRoundEngine{ + 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{ + 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, + nil, + nil, + ) + }(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) + } + + 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, + ) + } + } +} + +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, + nil, + nil, + ) + }(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, + nil, + nil, + ) + 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 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, + _ *[32]byte, + ) (*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 + payload *NativeTBTCSignerMaterialPayload + dkgResult *NativeTBTCSignerDKGResult + acceptScaffoldOptIn bool + expected string + substituted bool + expectError bool + expectScaffoldRefuse bool + }{ + { + name: "exact match", + payload: &NativeTBTCSignerMaterialPayload{ + KeyGroup: "group-1", + }, + dkgResult: &NativeTBTCSignerDKGResult{ + KeyGroup: "group-1", + }, + expected: "group-1", + substituted: false, + }, + { + name: "legacy source mismatch refused without opt-in", + payload: &NativeTBTCSignerMaterialPayload{ + KeyGroup: "legacy-group", + KeyGroupSource: NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + }, + dkgResult: &NativeTBTCSignerDKGResult{ + KeyGroup: "dkg-group", + }, + expectError: true, + expectScaffoldRefuse: true, + }, + { + name: "legacy source mismatch uses dkg key group with opt-in", + payload: &NativeTBTCSignerMaterialPayload{ + KeyGroup: "legacy-group", + KeyGroupSource: NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + }, + dkgResult: &NativeTBTCSignerDKGResult{ + KeyGroup: "dkg-group", + }, + acceptScaffoldOptIn: true, + expected: "dkg-group", + substituted: true, + }, + { + 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) { + if tc.acceptScaffoldOptIn { + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "true") + } else { + // Force the env to "" so a stray external value from a + // containing process cannot suppress the scaffold refusal + // during this test case. + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "") + } + + actual, substituted, err := buildTaggedTBTCSignerRoundKeyGroup(tc.payload, tc.dkgResult) + if tc.expectError { + if err == nil { + t.Fatal("expected error") + } + + if tc.expectScaffoldRefuse && + !strings.Contains(err.Error(), AcceptScaffoldKeyGroupEnvVar) { + t.Fatalf( + "expected scaffold-refusal error referencing %s; got: [%v]", + AcceptScaffoldKeyGroupEnvVar, + err, + ) + } + + 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, + ) + } + + 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, + ) + } + }) + } +} + +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", + 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 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 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: buildTaggedTBTCSignerValidTestSignature(0x11), + } + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + 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) + } + + 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{ + 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.Fatalf("unexpected error: [%v]", err) + } + + 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 signature bytes\nexpected: [%x]\nactual: [%x]", + expectedSignature, + marshaledSignature, + ) + } + + 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.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]", + "group-1", + engine.startKeyGroup, + ) + } + + 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") + } + + if len(observedEvents) != 0 { + t.Fatalf( + "did not expect fallback events\nactual: [%v]", + observedEvents, + ) + } + + 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]", + "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) + } + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_TaprootMerkleRoot( + t *testing.T, +) { + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + finalizeSignature: buildTaggedTBTCSignerValidTestSignature(0x22), + } + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + err := RegisterNativeTBTCSignerEngine(engine) + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + var taprootMerkleRoot [32]byte + taprootMerkleRoot[0] = 0xab + taprootMerkleRoot[31] = 0xcd + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + signature, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-1", + MemberIndex: 1, + GroupSize: 3, + DishonestThreshold: 1, + TaprootMerkleRoot: &taprootMerkleRoot, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte(`{"keyGroup":"group-1"}`), + }, + }) + if err != nil { + t.Fatalf("unexpected error: [%v]", err) + } + + if signature == nil { + t.Fatal("expected signature") + } + + if engine.startTaprootMerkleRoot == nil { + t.Fatal("expected StartSignRound taproot merkle root") + } + if !bytes.Equal(engine.startTaprootMerkleRoot[:], taprootMerkleRoot[:]) { + t.Fatalf( + "unexpected StartSignRound taproot merkle root\nexpected: [%x]\nactual: [%x]", + taprootMerkleRoot, + *engine.startTaprootMerkleRoot, + ) + } + + if engine.finalizeTaprootMerkleRoot == nil { + t.Fatal("expected FinalizeSignRound taproot merkle root") + } + if !bytes.Equal(engine.finalizeTaprootMerkleRoot[:], taprootMerkleRoot[:]) { + t.Fatalf( + "unexpected FinalizeSignRound taproot merkle root\nexpected: [%x]\nactual: [%x]", + taprootMerkleRoot, + *engine.finalizeTaprootMerkleRoot, + ) + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_InvalidCoarseSignatureFallsBack( + t *testing.T, +) { + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "true") + + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + finalizeSignature: []byte{0xaa}, + } + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + 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) + } + + 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{ + 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, + ) + } + + if len(observedCoarseSignatureEvents) != 0 { + t.Fatalf( + "did not expect coarse signature events\nactual: [%v]", + observedCoarseSignatureEvents, + ) + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_RefusesScaffoldMaterialWithoutOptIn( + t *testing.T, +) { + // Closes the persistence-vs-execution gap: even when the native + // tbtc-signer engine is registered (scaffold material on disk from a + // previous opted-in session is enough to reach this point), the FFI + // signing path must refuse to feed RunDKG placeholder participant + // pubkeys without an active operator opt-in. Force the env var off so + // any value inherited from the test runner's containing process cannot + // suppress the refusal. + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "") + + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + runDKGResult: &NativeTBTCSignerDKGResult{ + SessionID: "session-scaffold-refused", + KeyGroup: "group-from-dkg", + ParticipantCount: 3, + Threshold: 2, + CreatedAtUnix: 1, + }, + finalizeSignature: buildTaggedTBTCSignerValidTestSignature(0x22), + } + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + if err := RegisterNativeTBTCSignerEngine(engine); err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + primitive := &buildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive{} + + signature, err := primitive.Sign(nil, nil, &NativeExecutionFFISigningRequest{ + Message: big.NewInt(123), + SessionID: "session-scaffold-refused", + 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 scaffold-refusal error from FFI signing path") + } + if signature != nil { + t.Fatal("expected nil signature when scaffold path is refused") + } + + if !errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "expected ErrNativeCryptographyUnavailable wrap; got: [%v]", + err, + ) + } + if !strings.Contains(err.Error(), AcceptScaffoldKeyGroupEnvVar) { + t.Fatalf( + "expected scaffold-refusal error to reference %s; got: [%v]", + AcceptScaffoldKeyGroupEnvVar, + err, + ) + } + if !strings.Contains(err.Error(), NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey) { + t.Fatalf( + "expected scaffold-refusal error to reference %s; got: [%v]", + NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + err, + ) + } + + if engine.runDKGCalled { + t.Fatal( + "RunDKG must not be called when the scaffold opt-in flag is unset; " + + "refusing before the placeholder participant pubkeys are built " + + "is the whole point of the fence", + ) + } + if engine.startCalled { + t.Fatal("StartSignRound must not be called when the scaffold path is refused") + } + if engine.finalizeCalled { + t.Fatal( + "FinalizeSignRound must not be called when the scaffold path is refused", + ) + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_LegacyKeyGroupSourceUsesRunDKGResult( + t *testing.T, +) { + // Scaffold-era path: legacy-wallet-pubkey signer material is refused by + // default; the operator opt-in via AcceptScaffoldKeyGroupEnvVar is what + // lets this test exercise the substitution. Production deployments must + // never set this. + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "true") + + 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: 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{ + 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.Fatalf("unexpected error: [%v]", err) + } + + 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 signature bytes\nexpected: [%x]\nactual: [%x]", + expectedSignature, + marshaledSignature, + ) + } + + 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, + ) + } + + 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") + } + + 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( + 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, +) { + // Scaffold-era signing path requires explicit operator opt-in; this test + // exercises the engine-unavailable + no-legacy-share branch which lives + // past the scaffold fence. + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "true") + + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + 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", + 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 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") + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_AttemptVariationRunDKGConflictFallsBack( + t *testing.T, +) { + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "true") + + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + 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{ + Number: 2, + CoordinatorMemberIndex: 1, + 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, + ) + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_BootstrapVersion_AttemptVariationStartSignRoundConflictFallsBack( + t *testing.T, +) { + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "true") + + UnregisterNativeTBTCSignerEngine() + UnregisterNativeTBTCSignerFallbackObserver() + UnregisterNativeTBTCSignerCoarseSignatureObserver() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + t.Cleanup(UnregisterNativeTBTCSignerFallbackObserver) + t.Cleanup(UnregisterNativeTBTCSignerCoarseSignatureObserver) + + var firstSigningParticipants []uint16 + var observedSigningParticipants [][]uint16 + + engine := &mockBuildTaggedTBTCSignerEngine{ + version: "tbtc-signer/0.1.0-bootstrap", + finalizeSignature: buildTaggedTBTCSignerValidTestSignature(0x44), + 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, + _ *[32]byte, + ) (*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) + } + + 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{ + 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"}`), + }, + } + + firstSignature, err := primitive.Sign(nil, nil, baseRequest) + if err != nil { + t.Fatalf("unexpected first signing error: [%v]", err) + } + if firstSignature == nil { + t.Fatal("expected first signature") + } + + secondRequest := *baseRequest + secondRequest.Attempt = &Attempt{ + Number: 2, + CoordinatorMemberIndex: 1, + 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) != 1 { + t.Fatalf( + "unexpected fallback event count\nexpected: [%d]\nactual: [%d]", + 1, + len(observedEvents), + ) + } + + if !strings.Contains(observedEvents[0].Reason, "session_conflict") { + t.Fatalf( + "expected fallback reason to include session_conflict\nactual: [%s]", + observedEvents[0].Reason, + ) + } + + if len(observedCoarseSignatureEvents) != 1 { + t.Fatalf( + "unexpected coarse signature event count\nexpected: [%d]\nactual: [%d]", + 1, + len(observedCoarseSignatureEvents), + ) + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_InvalidAttemptPolicy_DoesNotFallback( + t *testing.T, +) { + // Scaffold-era signing path requires explicit operator opt-in; this test + // exercises the FFI flow's invalid-attempt-policy branch, which lives + // past the scaffold fence. + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "true") + + 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) + } + + testCases := []struct { + name string + attempt *Attempt + }{ + { + name: "zero attempt number", + attempt: &Attempt{ + Number: 0, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 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}, + }, + }, + } + + 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) + + 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: 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, + ) + } + }) + } +} + +func TestIsBuildTaggedTBTCSignerConsumedAttemptReplayError(t *testing.T) { + // Locking-mutex-free unit-coverage for the replay detector. Each case + // constructs an error in the shape that flows out of the FFI bridge today + // and asserts the detector's decision. + cases := []struct { + name string + err error + match bool + }{ + { + name: "nil error is not a replay", + err: nil, + match: false, + }, + { + name: "structured code wins over message wording", + err: fmt.Errorf( + "%w: tbtc-signer bridge operation [StartSignRound] failed: [%w]", + ErrNativeBridgeOperationFailed, + &buildTaggedTBTCSignerStructuredError{ + Code: buildTaggedTBTCSignerConsumedAttemptReplayErrorCode, + Message: "rust message wording is not load-bearing here", + }, + ), + match: true, + }, + { + name: "structured but different code does not match", + err: fmt.Errorf( + "%w: tbtc-signer bridge operation [StartSignRound] failed: [%w]", + ErrNativeBridgeOperationFailed, + &buildTaggedTBTCSignerStructuredError{ + Code: "session_conflict", + Message: "attempt_id [x] already consumed for sign attempt in session [y]", + }, + ), + match: false, + }, + { + name: "legacy substring still matches when code is missing", + err: fmt.Errorf( + "%w: tbtc-signer bridge operation [StartSignRound] failed: [%w]", + ErrNativeBridgeOperationFailed, + &buildTaggedTBTCSignerStructuredError{ + Code: "", + Message: "attempt_id [att-1] already consumed for sign attempt in session [sess-1]", + }, + ), + match: true, + }, + { + // Pre-dedicated-variant signer builds route the replay path + // through Validation, so the code on the wire is + // validation_error and only the message identifies replay. + name: "validation_error code with legacy wording is a replay", + err: fmt.Errorf( + "%w: tbtc-signer bridge operation [StartSignRound] failed: [%w]", + ErrNativeBridgeOperationFailed, + &buildTaggedTBTCSignerStructuredError{ + Code: "validation_error", + Message: "attempt_id [att-1] already consumed for sign attempt in session [sess-1]", + }, + ), + match: true, + }, + { + // A validation_error that is NOT the replay path must not be + // flagged as a replay even if surrounding error chain noise + // happens to mention attempt_id elsewhere. + name: "validation_error without legacy wording is not a replay", + err: fmt.Errorf( + "%w: tbtc-signer bridge operation [StartSignRound] failed: [%w]", + ErrNativeBridgeOperationFailed, + &buildTaggedTBTCSignerStructuredError{ + Code: "validation_error", + Message: "session_id is empty", + }, + ), + match: false, + }, + { + name: "legacy substring still matches when error is a plain wrapper", + err: fmt.Errorf( + "native FROST bridge operation failed: tbtc-signer bridge " + + "operation [StartSignRound] failed: [validation_error: " + + "attempt_id [att-1] already consumed for sign attempt in " + + "session [sess-1]]", + ), + match: true, + }, + { + name: "unrelated error is not a replay", + err: fmt.Errorf( + "%w: tbtc-signer bridge operation [StartSignRound] failed: [%w]", + ErrNativeBridgeOperationFailed, + &buildTaggedTBTCSignerStructuredError{ + Code: "validation_error", + Message: "session_id is empty", + }, + ), + match: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := isBuildTaggedTBTCSignerConsumedAttemptReplayError(tc.err); got != tc.match { + t.Fatalf( + "detector returned [%v]; expected [%v] for error [%v]", + got, + tc.match, + tc.err, + ) + } + }) + } +} + +func TestBuildTaggedTBTCSignerErrorPayload(t *testing.T) { + cases := []struct { + name string + payload []byte + code string + // Substring expected in the rendered Message. Empty means we don't + // assert beyond Code presence. + messageSubstring string + }{ + { + name: "decodes structured envelope", + payload: []byte(`{"code":"consumed_attempt_replay","message":"attempt_id [a] already consumed"}`), + code: "consumed_attempt_replay", + messageSubstring: "already consumed", + }, + { + name: "legacy validation_error code is preserved", + payload: []byte(`{"code":"validation_error","message":"session_id is empty"}`), + code: "validation_error", + messageSubstring: "session_id is empty", + }, + { + name: "message-only payload leaves Code empty", + payload: []byte(`{"message":"opaque message"}`), + code: "", + messageSubstring: "opaque message", + }, + { + name: "completely empty envelope surfaces the raw payload", + payload: []byte(`{}`), + code: "", + messageSubstring: "empty error payload", + }, + { + name: "non-JSON payload is reported as a decode failure", + payload: []byte(`not json`), + code: "", + messageSubstring: "cannot decode error payload", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + structured := buildTaggedTBTCSignerErrorPayload(tc.payload) + if structured == nil { + t.Fatal("expected non-nil structured error") + } + if structured.Code != tc.code { + t.Fatalf( + "unexpected Code\nexpected: [%s]\nactual: [%s]", + tc.code, + structured.Code, + ) + } + if tc.messageSubstring != "" && + !strings.Contains(structured.Message, tc.messageSubstring) { + t.Fatalf( + "Message missing expected substring\nexpected substring: [%s]\nactual: [%s]", + tc.messageSubstring, + structured.Message, + ) + } + }) + } +} + +func TestBuildTaggedLegacyCompatibleNativeExecutionFFISigningPrimitive_Sign_TBTCSignerPath_ConsumedAttemptReplay_DoesNotFallback( + t *testing.T, +) { + // Scaffold-era signing path requires explicit operator opt-in; this test + // exercises the FFI flow's consumed-attempt-replay branch, which lives + // past the scaffold fence. + t.Setenv(AcceptScaffoldKeyGroupEnvVar, "true") + + 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_dkg_engine_frost_native.go b/pkg/frost/signing/native_frost_dkg_engine_frost_native.go new file mode 100644 index 0000000000..c3cca7ade5 --- /dev/null +++ b/pkg/frost/signing/native_frost_dkg_engine_frost_native.go @@ -0,0 +1,54 @@ +//go:build frost_native + +package signing + +// NativeFROSTDKGRound1Package is the public package broadcast during FROST DKG +// round one. +type NativeFROSTDKGRound1Package struct { + Identifier string `json:"identifier"` + Data []byte `json:"data"` +} + +// NativeFROSTDKGRound2Package is the package sent to a specific DKG +// participant during FROST DKG round two. +type NativeFROSTDKGRound2Package struct { + // Identifier is the recipient participant identifier embedded by the + // native DKG package. + Identifier string `json:"identifier"` + // SenderIdentifier is filled by the Go coordinator for packages received + // from peers. The tbtc-signer DKG Part3 request keys round-two packages by + // sender while the package itself carries the recipient. + SenderIdentifier string `json:"senderIdentifier,omitempty"` + Data []byte `json:"data"` +} + +// NativeFROSTDKGRound1SecretPackage is signer-local secret material produced +// in DKG round one. It must never be broadcast. +type NativeFROSTDKGRound1SecretPackage struct { + Data []byte `json:"data"` +} + +// NativeFROSTDKGRound2SecretPackage is signer-local secret material produced +// in DKG round two. It must never be broadcast. +type NativeFROSTDKGRound2SecretPackage struct { + Data []byte `json:"data"` +} + +// NativeFROSTDKGPart1Result is the output of native FROST DKG part one. +type NativeFROSTDKGPart1Result struct { + SecretPackage *NativeFROSTDKGRound1SecretPackage `json:"secretPackage"` + Package *NativeFROSTDKGRound1Package `json:"package"` +} + +// NativeFROSTDKGPart2Result is the output of native FROST DKG part two. +type NativeFROSTDKGPart2Result struct { + SecretPackage *NativeFROSTDKGRound2SecretPackage `json:"secretPackage"` + Packages []*NativeFROSTDKGRound2Package `json:"packages"` +} + +// NativeFROSTDKGResult is the final native FROST DKG output consumed by the +// signing runtime and persisted by keep-core. +type NativeFROSTDKGResult struct { + KeyPackage *NativeFROSTKeyPackage `json:"keyPackage"` + PublicKeyPackage *NativeFROSTPublicKeyPackage `json:"publicKeyPackage"` +} 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..45e6a9855a --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_frost_native.go @@ -0,0 +1,63 @@ +//go:build frost_native + +package signing + +const ( + // NativeSignerMaterialFormatFrostUniFFIV2 is the unsupported generic UniFFI + // FROST signer-material envelope. It is kept as a string constant so stale + // local/test material can be identified and rejected explicitly. + NativeSignerMaterialFormatFrostUniFFIV2 = "frost-uniffi-v2" +) + +// 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. FROST signing +// nonces are one-time secrets. The generic UniFFI signing protocol that used +// this type is no longer registered; it remains only as a tbtc-signer FFI DTO. +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"` +} + +type nativeFROSTCommitment struct { + Identifier string + Data []byte +} + +type nativeFROSTSignatureShare struct { + Identifier string + Data []byte +} + +func zeroBytes(data []byte) { + for i := range data { + data[i] = 0 + } +} 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..e70e1b75a4 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -0,0 +1,2377 @@ +//go:build frost_native && frost_tbtc_signer && cgo + +package signing + +/* +#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_version_fn)(void); +typedef TbtcSignerResult (*tbtc_run_dkg_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef TbtcSignerResult (*tbtc_dkg_part1_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef TbtcSignerResult (*tbtc_dkg_part2_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef TbtcSignerResult (*tbtc_dkg_part3_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef TbtcSignerResult (*tbtc_generate_nonces_and_commitments_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef TbtcSignerResult (*tbtc_new_signing_package_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef TbtcSignerResult (*tbtc_sign_share_fn)( + const uint8_t* request_ptr, + size_t request_len +); +typedef TbtcSignerResult (*tbtc_aggregate_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 +); +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) { + TbtcSignerResult result; + result.status_code = -1; + result.buffer.ptr = NULL; + result.buffer.len = 0; + 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, + "frost_tbtc_run_dkg" + ); + if (run_dkg == NULL) { + return unavailable_tbtc_signer_result(); + } + + return run_dkg(request_ptr, request_len); +} + +static TbtcSignerResult tbtc_signer_dkg_part1(const uint8_t* request_ptr, size_t request_len) { + tbtc_dkg_part1_fn dkg_part1 = (tbtc_dkg_part1_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_dkg_part1" + ); + if (dkg_part1 == NULL) { + return unavailable_tbtc_signer_result(); + } + + return dkg_part1(request_ptr, request_len); +} + +static TbtcSignerResult tbtc_signer_dkg_part2(const uint8_t* request_ptr, size_t request_len) { + tbtc_dkg_part2_fn dkg_part2 = (tbtc_dkg_part2_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_dkg_part2" + ); + if (dkg_part2 == NULL) { + return unavailable_tbtc_signer_result(); + } + + return dkg_part2(request_ptr, request_len); +} + +static TbtcSignerResult tbtc_signer_dkg_part3(const uint8_t* request_ptr, size_t request_len) { + tbtc_dkg_part3_fn dkg_part3 = (tbtc_dkg_part3_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_dkg_part3" + ); + if (dkg_part3 == NULL) { + return unavailable_tbtc_signer_result(); + } + + return dkg_part3(request_ptr, request_len); +} + +static TbtcSignerResult tbtc_signer_generate_nonces_and_commitments(const uint8_t* request_ptr, size_t request_len) { + tbtc_generate_nonces_and_commitments_fn generate_nonces_and_commitments = + (tbtc_generate_nonces_and_commitments_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_generate_nonces_and_commitments" + ); + if (generate_nonces_and_commitments == NULL) { + return unavailable_tbtc_signer_result(); + } + + return generate_nonces_and_commitments(request_ptr, request_len); +} + +static TbtcSignerResult tbtc_signer_new_signing_package(const uint8_t* request_ptr, size_t request_len) { + tbtc_new_signing_package_fn new_signing_package = (tbtc_new_signing_package_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_new_signing_package" + ); + if (new_signing_package == NULL) { + return unavailable_tbtc_signer_result(); + } + + return new_signing_package(request_ptr, request_len); +} + +static TbtcSignerResult tbtc_signer_sign_share(const uint8_t* request_ptr, size_t request_len) { + tbtc_sign_share_fn sign_share = (tbtc_sign_share_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_sign_share" + ); + if (sign_share == NULL) { + return unavailable_tbtc_signer_result(); + } + + return sign_share(request_ptr, request_len); +} + +static TbtcSignerResult tbtc_signer_aggregate(const uint8_t* request_ptr, size_t request_len) { + tbtc_aggregate_fn aggregate = (tbtc_aggregate_fn)dlsym( + RTLD_DEFAULT, + "frost_tbtc_aggregate" + ); + if (aggregate == NULL) { + return unavailable_tbtc_signer_result(); + } + + return aggregate(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, + "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 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, + "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 buildTaggedTBTCSignerRunDKGRequest struct { + SessionID string `json:"session_id"` + Participants []buildTaggedTBTCSignerDKGParticipant `json:"participants"` + Threshold uint16 `json:"threshold"` + DKGSeedHex *string `json:"dkg_seed_hex,omitempty"` +} + +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 buildTaggedTBTCSignerDKGPart1Request struct { + ParticipantIdentifier string `json:"participant_identifier"` + MaxSigners uint16 `json:"max_signers"` + MinSigners uint16 `json:"min_signers"` +} + +type buildTaggedTBTCSignerDKGRound1Package struct { + Identifier string `json:"identifier"` + PackageHex string `json:"package_hex"` +} + +type buildTaggedTBTCSignerDKGRound2Package struct { + Identifier string `json:"identifier"` + SenderIdentifier *string `json:"sender_identifier,omitempty"` + PackageHex string `json:"package_hex"` +} + +type buildTaggedTBTCSignerDKGPart1Response struct { + SecretPackageHex string `json:"secret_package_hex"` + Package *buildTaggedTBTCSignerDKGRound1Package `json:"package"` +} + +type buildTaggedTBTCSignerDKGPart2Request struct { + SecretPackageHex string `json:"secret_package_hex"` + Round1Packages []buildTaggedTBTCSignerDKGRound1Package `json:"round1_packages"` +} + +type buildTaggedTBTCSignerDKGPart2Response struct { + SecretPackageHex string `json:"secret_package_hex"` + Packages []buildTaggedTBTCSignerDKGRound2Package `json:"packages"` +} + +type buildTaggedTBTCSignerDKGPart3Request struct { + SecretPackageHex string `json:"secret_package_hex"` + Round1Packages []buildTaggedTBTCSignerDKGRound1Package `json:"round1_packages"` + Round2Packages []buildTaggedTBTCSignerDKGRound2Package `json:"round2_packages"` +} + +type buildTaggedTBTCSignerNativeFROSTKeyPackage struct { + Identifier string `json:"identifier"` + DataHex string `json:"data_hex"` +} + +type buildTaggedTBTCSignerNativeFROSTPublicKeyPackage struct { + VerifyingShares map[string]string `json:"verifying_shares"` + VerifyingKey string `json:"verifying_key"` +} + +type buildTaggedTBTCSignerDKGPart3Response struct { + KeyPackage *buildTaggedTBTCSignerNativeFROSTKeyPackage `json:"key_package"` + PublicKeyPackage *buildTaggedTBTCSignerNativeFROSTPublicKeyPackage `json:"public_key_package"` +} + +type buildTaggedTBTCSignerNativeFROSTCommitment struct { + Identifier string `json:"identifier"` + DataHex string `json:"data_hex"` +} + +type buildTaggedTBTCSignerNativeFROSTSignatureShare struct { + Identifier string `json:"identifier"` + DataHex string `json:"data_hex"` +} + +type buildTaggedTBTCSignerGenerateNoncesRequest struct { + KeyPackageIdentifier string `json:"key_package_identifier"` + KeyPackageHex string `json:"key_package_hex"` +} + +type buildTaggedTBTCSignerGenerateNoncesResponse struct { + NoncesHex string `json:"nonces_hex"` + Commitment *buildTaggedTBTCSignerNativeFROSTCommitment `json:"commitment"` +} + +type buildTaggedTBTCSignerNewSigningPackageRequest struct { + MessageHex string `json:"message_hex"` + Commitments []buildTaggedTBTCSignerNativeFROSTCommitment `json:"commitments"` +} + +type buildTaggedTBTCSignerNewSigningPackageResponse struct { + SigningPackageHex string `json:"signing_package_hex"` +} + +type buildTaggedTBTCSignerSignShareRequest struct { + SigningPackageHex string `json:"signing_package_hex"` + NoncesHex string `json:"nonces_hex"` + KeyPackageIdentifier string `json:"key_package_identifier"` + KeyPackageHex string `json:"key_package_hex"` +} + +type buildTaggedTBTCSignerSignShareResponse struct { + SignatureShare *buildTaggedTBTCSignerNativeFROSTSignatureShare `json:"signature_share"` +} + +type buildTaggedTBTCSignerAggregateRequest struct { + SigningPackageHex string `json:"signing_package_hex"` + SignatureShares []buildTaggedTBTCSignerNativeFROSTSignatureShare `json:"signature_shares"` + PublicKeyPackage *buildTaggedTBTCSignerNativeFROSTPublicKeyPackage `json:"public_key_package"` +} + +type buildTaggedTBTCSignerAggregateResponse struct { + SignatureHex string `json:"signature_hex"` +} + +type buildTaggedTBTCSignerStartSignRoundRequest struct { + SessionID string `json:"session_id"` + MemberIdentifier uint16 `json:"member_identifier"` + MessageHex string `json:"message_hex"` + KeyGroup string `json:"key_group"` + TaprootMerkleRootHex *string `json:"taproot_merkle_root_hex,omitempty"` + SigningParticipants []uint16 `json:"signing_participants,omitempty"` +} + +type buildTaggedTBTCSignerStartSignRoundResponse struct { + SessionID string `json:"session_id"` + 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"` +} + +type buildTaggedTBTCSignerFinalizeSignRoundRequest struct { + SessionID string `json:"session_id"` + TaprootMerkleRootHex *string `json:"taproot_merkle_root_hex,omitempty"` + 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"` +} + +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 { + engine := &buildTaggedTBTCSignerEngine{} + + // Do not register the tbtc-signer bridge as the generic UniFFI-shaped + // FROST DKG/signing engine. That path persists `frost-uniffi-v2` wallet + // material, which cannot produce Taproot-tweaked signatures. A wallet + // using that material can accept Taproot deposits that are effectively + // unsweepable, so this must fail before new FROST wallet material exists. + // New FROST wallets in this build must use the coarse + // `frost-tbtc-signer-v1` material path exclusively. + return RegisterNativeTBTCSignerEngine(engine) +} + +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, + 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) RunDKGWithSeed( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + dkgSeedHex string, +) (*NativeTBTCSignerDKGResult, error) { + requestPayload, err := buildTaggedTBTCSignerRunDKGRequestPayloadWithSeed( + sessionID, + participants, + threshold, + dkgSeedHex, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerRunDKG(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerRunDKGResponse(responsePayload) +} + +func (bttse *buildTaggedTBTCSignerEngine) Part1( + participantIdentifier string, + maxSigners uint16, + minSigners uint16, +) (*NativeFROSTDKGPart1Result, error) { + requestPayload, err := buildTaggedTBTCSignerDKGPart1RequestPayload( + participantIdentifier, + maxSigners, + minSigners, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerDKGPart1(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerDKGPart1Response(responsePayload) +} + +func (bttse *buildTaggedTBTCSignerEngine) Part2( + secretPackage *NativeFROSTDKGRound1SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, +) (*NativeFROSTDKGPart2Result, error) { + requestPayload, err := buildTaggedTBTCSignerDKGPart2RequestPayload( + secretPackage, + round1Packages, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerDKGPart2(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerDKGPart2Response(responsePayload) +} + +func (bttse *buildTaggedTBTCSignerEngine) Part3( + secretPackage *NativeFROSTDKGRound2SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + round2Packages []*NativeFROSTDKGRound2Package, +) (*NativeFROSTDKGResult, error) { + requestPayload, err := buildTaggedTBTCSignerDKGPart3RequestPayload( + secretPackage, + round1Packages, + round2Packages, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerDKGPart3(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerDKGPart3Response(responsePayload) +} + +func (bttse *buildTaggedTBTCSignerEngine) GenerateNoncesAndCommitments( + keyPackageIdentifier string, + keyPackageData []byte, +) (noncesData []byte, commitmentIdentifier string, commitmentData []byte, err error) { + requestPayload, err := buildTaggedTBTCSignerGenerateNoncesRequestPayload( + keyPackageIdentifier, + keyPackageData, + ) + if err != nil { + return nil, "", nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerGenerateNoncesAndCommitments( + requestPayload, + ) + if err != nil { + return nil, "", nil, err + } + defer zeroBytes(responsePayload) + + return decodeBuildTaggedTBTCSignerGenerateNoncesResponse(responsePayload) +} + +func (bttse *buildTaggedTBTCSignerEngine) NewSigningPackage( + message []byte, + commitments []nativeFROSTCommitment, +) (signingPackageData []byte, err error) { + requestPayload, err := buildTaggedTBTCSignerNewSigningPackageRequestPayload( + message, + commitments, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerNewSigningPackage(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerNewSigningPackageResponse(responsePayload) +} + +func (bttse *buildTaggedTBTCSignerEngine) Sign( + signingPackageData []byte, + noncesData []byte, + keyPackageIdentifier string, + keyPackageData []byte, +) (signatureShareIdentifier string, signatureShareData []byte, err error) { + defer zeroBytes(noncesData) + + requestPayload, err := buildTaggedTBTCSignerSignShareRequestPayload( + signingPackageData, + noncesData, + keyPackageIdentifier, + keyPackageData, + ) + if err != nil { + return "", nil, err + } + defer zeroBytes(requestPayload) + + responsePayload, err := callBuildTaggedTBTCSignerSignShare(requestPayload) + if err != nil { + return "", nil, err + } + + return decodeBuildTaggedTBTCSignerSignShareResponse(responsePayload) +} + +func (bttse *buildTaggedTBTCSignerEngine) Aggregate( + signingPackageData []byte, + signatureShares []nativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, +) (signature []byte, err error) { + requestPayload, err := buildTaggedTBTCSignerAggregateRequestPayload( + signingPackageData, + signatureShares, + publicKeyPackage, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerAggregate(requestPayload) + if err != nil { + return nil, err + } + + return decodeBuildTaggedTBTCSignerAggregateResponse(responsePayload) +} + +func (bttse *buildTaggedTBTCSignerEngine) StartSignRound( + sessionID string, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, + taprootMerkleRoot *[32]byte, +) (*NativeTBTCSignerRoundState, error) { + requestPayload, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( + sessionID, + memberIdentifier, + message, + keyGroup, + signingParticipants, + taprootMerkleRoot, + ) + 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, + taprootMerkleRoot *[32]byte, +) ([]byte, error) { + requestPayload, err := buildTaggedTBTCSignerFinalizeSignRoundRequestPayload( + sessionID, + roundContributions, + taprootMerkleRoot, + ) + if err != nil { + return nil, err + } + + responsePayload, err := callBuildTaggedTBTCSignerFinalizeSignRound(requestPayload) + if err != nil { + return nil, err + } + + 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", + ErrNativeCryptographyUnavailable, + operation, + ) +} + +func buildTaggedTBTCSignerOperationError( + 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, + threshold uint16, +) ([]byte, error) { + return buildTaggedTBTCSignerRunDKGRequestPayloadWithOptionalSeed( + sessionID, + participants, + threshold, + nil, + ) +} + +func buildTaggedTBTCSignerRunDKGRequestPayloadWithSeed( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + dkgSeedHex string, +) ([]byte, error) { + if dkgSeedHex == "" { + return nil, buildTaggedTBTCSignerOperationError( + "RunDKG", + "DKG seed hex is empty", + ) + } + + return buildTaggedTBTCSignerRunDKGRequestPayloadWithOptionalSeed( + sessionID, + participants, + threshold, + &dkgSeedHex, + ) +} + +func buildTaggedTBTCSignerRunDKGRequestPayloadWithOptionalSeed( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + dkgSeedHex *string, +) ([]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, + DKGSeedHex: dkgSeedHex, + } + + 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 buildTaggedTBTCSignerDKGPart1RequestPayload( + participantIdentifier string, + maxSigners uint16, + minSigners uint16, +) ([]byte, error) { + if participantIdentifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart1", + "participant identifier is empty", + ) + } + if maxSigners == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart1", + "max signers is zero", + ) + } + if minSigners == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart1", + "min signers is zero", + ) + } + if minSigners > maxSigners { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart1", + "min signers exceeds max signers", + ) + } + + return buildTaggedTBTCSignerMarshalRequest( + "DKGPart1", + buildTaggedTBTCSignerDKGPart1Request{ + ParticipantIdentifier: participantIdentifier, + MaxSigners: maxSigners, + MinSigners: minSigners, + }, + ) +} + +func decodeBuildTaggedTBTCSignerDKGPart1Response( + responsePayload []byte, +) (*NativeFROSTDKGPart1Result, error) { + var response buildTaggedTBTCSignerDKGPart1Response + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart1", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + secretPackageData, err := buildTaggedTBTCSignerDecodeHexField( + "DKGPart1", + "response secret package", + response.SecretPackageHex, + ) + if err != nil { + return nil, err + } + round1Package, err := decodeBuildTaggedTBTCSignerDKGRound1Package( + "DKGPart1", + "response package", + response.Package, + ) + if err != nil { + return nil, err + } + + return &NativeFROSTDKGPart1Result{ + SecretPackage: &NativeFROSTDKGRound1SecretPackage{ + Data: secretPackageData, + }, + Package: round1Package, + }, nil +} + +func buildTaggedTBTCSignerDKGPart2RequestPayload( + secretPackage *NativeFROSTDKGRound1SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, +) ([]byte, error) { + if secretPackage == nil { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart2", + "secret package is nil", + ) + } + if len(secretPackage.Data) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart2", + "secret package data is empty", + ) + } + + requestPackages, err := buildTaggedTBTCSignerDKGRound1PackagePayloads( + "DKGPart2", + round1Packages, + ) + if err != nil { + return nil, err + } + + return buildTaggedTBTCSignerMarshalRequest( + "DKGPart2", + buildTaggedTBTCSignerDKGPart2Request{ + SecretPackageHex: hex.EncodeToString(secretPackage.Data), + Round1Packages: requestPackages, + }, + ) +} + +func decodeBuildTaggedTBTCSignerDKGPart2Response( + responsePayload []byte, +) (*NativeFROSTDKGPart2Result, error) { + var response buildTaggedTBTCSignerDKGPart2Response + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart2", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + secretPackageData, err := buildTaggedTBTCSignerDecodeHexField( + "DKGPart2", + "response secret package", + response.SecretPackageHex, + ) + if err != nil { + return nil, err + } + if len(response.Packages) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart2", + "response packages are empty", + ) + } + + packages := make([]*NativeFROSTDKGRound2Package, 0, len(response.Packages)) + for i := range response.Packages { + pkg, err := decodeBuildTaggedTBTCSignerDKGRound2Package( + "DKGPart2", + fmt.Sprintf("response package [%d]", i), + &response.Packages[i], + false, + ) + if err != nil { + return nil, err + } + packages = append(packages, pkg) + } + + return &NativeFROSTDKGPart2Result{ + SecretPackage: &NativeFROSTDKGRound2SecretPackage{ + Data: secretPackageData, + }, + Packages: packages, + }, nil +} + +func buildTaggedTBTCSignerDKGPart3RequestPayload( + secretPackage *NativeFROSTDKGRound2SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + round2Packages []*NativeFROSTDKGRound2Package, +) ([]byte, error) { + if secretPackage == nil { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart3", + "secret package is nil", + ) + } + if len(secretPackage.Data) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart3", + "secret package data is empty", + ) + } + + requestRound1Packages, err := buildTaggedTBTCSignerDKGRound1PackagePayloads( + "DKGPart3", + round1Packages, + ) + if err != nil { + return nil, err + } + requestRound2Packages, err := buildTaggedTBTCSignerDKGRound2PackagePayloads( + "DKGPart3", + round2Packages, + true, + ) + if err != nil { + return nil, err + } + + return buildTaggedTBTCSignerMarshalRequest( + "DKGPart3", + buildTaggedTBTCSignerDKGPart3Request{ + SecretPackageHex: hex.EncodeToString(secretPackage.Data), + Round1Packages: requestRound1Packages, + Round2Packages: requestRound2Packages, + }, + ) +} + +func decodeBuildTaggedTBTCSignerDKGPart3Response( + responsePayload []byte, +) (*NativeFROSTDKGResult, error) { + var response buildTaggedTBTCSignerDKGPart3Response + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart3", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + if response.KeyPackage == nil { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart3", + "response key package is nil", + ) + } + if response.KeyPackage.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart3", + "response key package identifier is empty", + ) + } + keyPackageData, err := buildTaggedTBTCSignerDecodeHexField( + "DKGPart3", + "response key package data", + response.KeyPackage.DataHex, + ) + if err != nil { + return nil, err + } + if response.PublicKeyPackage == nil { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart3", + "response public key package is nil", + ) + } + if response.PublicKeyPackage.VerifyingKey == "" { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart3", + "response public key package verifying key is empty", + ) + } + if len(response.PublicKeyPackage.VerifyingShares) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "DKGPart3", + "response public key package verifying shares are empty", + ) + } + + return &NativeFROSTDKGResult{ + KeyPackage: &NativeFROSTKeyPackage{ + Identifier: response.KeyPackage.Identifier, + Data: keyPackageData, + }, + PublicKeyPackage: &NativeFROSTPublicKeyPackage{ + VerifyingShares: appendBuildTaggedTBTCSignerStringMap( + response.PublicKeyPackage.VerifyingShares, + ), + VerifyingKey: response.PublicKeyPackage.VerifyingKey, + }, + }, nil +} + +func buildTaggedTBTCSignerGenerateNoncesRequestPayload( + keyPackageIdentifier string, + keyPackageData []byte, +) ([]byte, error) { + if keyPackageIdentifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + "GenerateNoncesAndCommitments", + "key package identifier is empty", + ) + } + if len(keyPackageData) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "GenerateNoncesAndCommitments", + "key package data is empty", + ) + } + + return buildTaggedTBTCSignerMarshalRequest( + "GenerateNoncesAndCommitments", + buildTaggedTBTCSignerGenerateNoncesRequest{ + KeyPackageIdentifier: keyPackageIdentifier, + KeyPackageHex: hex.EncodeToString(keyPackageData), + }, + ) +} + +func decodeBuildTaggedTBTCSignerGenerateNoncesResponse( + responsePayload []byte, +) (noncesData []byte, commitmentIdentifier string, commitmentData []byte, err error) { + var response buildTaggedTBTCSignerGenerateNoncesResponse + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, "", nil, buildTaggedTBTCSignerOperationError( + "GenerateNoncesAndCommitments", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + noncesData, err = buildTaggedTBTCSignerDecodeHexField( + "GenerateNoncesAndCommitments", + "response nonces", + response.NoncesHex, + ) + if err != nil { + return nil, "", nil, err + } + commitment, err := decodeBuildTaggedTBTCSignerCommitment( + "GenerateNoncesAndCommitments", + "response commitment", + response.Commitment, + ) + if err != nil { + return nil, "", nil, err + } + + return noncesData, commitment.Identifier, commitment.Data, nil +} + +func buildTaggedTBTCSignerNewSigningPackageRequestPayload( + message []byte, + commitments []nativeFROSTCommitment, +) ([]byte, error) { + if len(commitments) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "NewSigningPackage", + "commitments are empty", + ) + } + + requestCommitments := make( + []buildTaggedTBTCSignerNativeFROSTCommitment, + 0, + len(commitments), + ) + for i, commitment := range commitments { + if commitment.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + "NewSigningPackage", + fmt.Sprintf("commitment [%d] identifier is empty", i), + ) + } + if len(commitment.Data) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "NewSigningPackage", + fmt.Sprintf("commitment [%d] data is empty", i), + ) + } + requestCommitments = append( + requestCommitments, + buildTaggedTBTCSignerNativeFROSTCommitment{ + Identifier: commitment.Identifier, + DataHex: hex.EncodeToString(commitment.Data), + }, + ) + } + + return buildTaggedTBTCSignerMarshalRequest( + "NewSigningPackage", + buildTaggedTBTCSignerNewSigningPackageRequest{ + MessageHex: hex.EncodeToString(message), + Commitments: requestCommitments, + }, + ) +} + +func decodeBuildTaggedTBTCSignerNewSigningPackageResponse( + responsePayload []byte, +) ([]byte, error) { + var response buildTaggedTBTCSignerNewSigningPackageResponse + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "NewSigningPackage", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + return buildTaggedTBTCSignerDecodeHexField( + "NewSigningPackage", + "response signing package", + response.SigningPackageHex, + ) +} + +func buildTaggedTBTCSignerSignShareRequestPayload( + signingPackageData []byte, + noncesData []byte, + keyPackageIdentifier string, + keyPackageData []byte, +) ([]byte, error) { + if len(signingPackageData) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "SignShare", + "signing package data is empty", + ) + } + if len(noncesData) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "SignShare", + "nonces data is empty", + ) + } + if keyPackageIdentifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + "SignShare", + "key package identifier is empty", + ) + } + if len(keyPackageData) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "SignShare", + "key package data is empty", + ) + } + + return buildTaggedTBTCSignerMarshalRequest( + "SignShare", + buildTaggedTBTCSignerSignShareRequest{ + SigningPackageHex: hex.EncodeToString(signingPackageData), + NoncesHex: hex.EncodeToString(noncesData), + KeyPackageIdentifier: keyPackageIdentifier, + KeyPackageHex: hex.EncodeToString(keyPackageData), + }, + ) +} + +func decodeBuildTaggedTBTCSignerSignShareResponse( + responsePayload []byte, +) (string, []byte, error) { + var response buildTaggedTBTCSignerSignShareResponse + if err := json.Unmarshal(responsePayload, &response); err != nil { + return "", nil, buildTaggedTBTCSignerOperationError( + "SignShare", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + signatureShare, err := decodeBuildTaggedTBTCSignerSignatureShare( + "SignShare", + "response signature share", + response.SignatureShare, + ) + if err != nil { + return "", nil, err + } + + return signatureShare.Identifier, signatureShare.Data, nil +} + +func buildTaggedTBTCSignerAggregateRequestPayload( + signingPackageData []byte, + signatureShares []nativeFROSTSignatureShare, + publicKeyPackage *NativeFROSTPublicKeyPackage, +) ([]byte, error) { + if len(signingPackageData) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "Aggregate", + "signing package data is empty", + ) + } + if len(signatureShares) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "Aggregate", + "signature shares are empty", + ) + } + if publicKeyPackage == nil { + return nil, buildTaggedTBTCSignerOperationError( + "Aggregate", + "public key package is nil", + ) + } + if publicKeyPackage.VerifyingKey == "" { + return nil, buildTaggedTBTCSignerOperationError( + "Aggregate", + "public key package verifying key is empty", + ) + } + if len(publicKeyPackage.VerifyingShares) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "Aggregate", + "public key package verifying shares are empty", + ) + } + + requestShares := make( + []buildTaggedTBTCSignerNativeFROSTSignatureShare, + 0, + len(signatureShares), + ) + for i, signatureShare := range signatureShares { + if signatureShare.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + "Aggregate", + fmt.Sprintf("signature share [%d] identifier is empty", i), + ) + } + if len(signatureShare.Data) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "Aggregate", + fmt.Sprintf("signature share [%d] data is empty", i), + ) + } + requestShares = append( + requestShares, + buildTaggedTBTCSignerNativeFROSTSignatureShare{ + Identifier: signatureShare.Identifier, + DataHex: hex.EncodeToString(signatureShare.Data), + }, + ) + } + + return buildTaggedTBTCSignerMarshalRequest( + "Aggregate", + buildTaggedTBTCSignerAggregateRequest{ + SigningPackageHex: hex.EncodeToString(signingPackageData), + SignatureShares: requestShares, + PublicKeyPackage: &buildTaggedTBTCSignerNativeFROSTPublicKeyPackage{ + VerifyingShares: appendBuildTaggedTBTCSignerStringMap( + publicKeyPackage.VerifyingShares, + ), + VerifyingKey: publicKeyPackage.VerifyingKey, + }, + }, + ) +} + +func decodeBuildTaggedTBTCSignerAggregateResponse( + responsePayload []byte, +) ([]byte, error) { + var response buildTaggedTBTCSignerAggregateResponse + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, buildTaggedTBTCSignerOperationError( + "Aggregate", + fmt.Sprintf("cannot decode response payload: %v", err), + ) + } + + return buildTaggedTBTCSignerDecodeHexField( + "Aggregate", + "response signature", + response.SignatureHex, + ) +} + +func buildTaggedTBTCSignerMarshalRequest( + operation string, + request interface{}, +) ([]byte, error) { + payload, err := json.Marshal(request) + if err != nil { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("cannot marshal request: %v", err), + ) + } + + return payload, nil +} + +func buildTaggedTBTCSignerDecodeHexField( + operation string, + fieldName string, + value string, +) ([]byte, error) { + if value == "" { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s is empty", fieldName), + ) + } + + data, err := hex.DecodeString(value) + if err != nil { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s is invalid hex: %v", fieldName, err), + ) + } + if len(data) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s decoded to empty bytes", fieldName), + ) + } + + return data, nil +} + +func buildTaggedTBTCSignerDKGRound1PackagePayloads( + operation string, + packages []*NativeFROSTDKGRound1Package, +) ([]buildTaggedTBTCSignerDKGRound1Package, error) { + if len(packages) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + "round-one packages are empty", + ) + } + + payloads := make( + []buildTaggedTBTCSignerDKGRound1Package, + 0, + len(packages), + ) + for i, pkg := range packages { + if pkg == nil { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("round-one package [%d] is nil", i), + ) + } + if pkg.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("round-one package [%d] identifier is empty", i), + ) + } + if len(pkg.Data) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("round-one package [%d] data is empty", i), + ) + } + payloads = append(payloads, buildTaggedTBTCSignerDKGRound1Package{ + Identifier: pkg.Identifier, + PackageHex: hex.EncodeToString(pkg.Data), + }) + } + + return payloads, nil +} + +func buildTaggedTBTCSignerDKGRound2PackagePayloads( + operation string, + packages []*NativeFROSTDKGRound2Package, + requireSenderIdentifier bool, +) ([]buildTaggedTBTCSignerDKGRound2Package, error) { + if len(packages) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + "round-two packages are empty", + ) + } + + payloads := make( + []buildTaggedTBTCSignerDKGRound2Package, + 0, + len(packages), + ) + for i, pkg := range packages { + if pkg == nil { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("round-two package [%d] is nil", i), + ) + } + if pkg.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("round-two package [%d] identifier is empty", i), + ) + } + if requireSenderIdentifier && pkg.SenderIdentifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("round-two package [%d] sender identifier is empty", i), + ) + } + if len(pkg.Data) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("round-two package [%d] data is empty", i), + ) + } + + var senderIdentifier *string + if pkg.SenderIdentifier != "" { + copied := pkg.SenderIdentifier + senderIdentifier = &copied + } + payloads = append(payloads, buildTaggedTBTCSignerDKGRound2Package{ + Identifier: pkg.Identifier, + SenderIdentifier: senderIdentifier, + PackageHex: hex.EncodeToString(pkg.Data), + }) + } + + return payloads, nil +} + +func decodeBuildTaggedTBTCSignerDKGRound1Package( + operation string, + fieldName string, + pkg *buildTaggedTBTCSignerDKGRound1Package, +) (*NativeFROSTDKGRound1Package, error) { + if pkg == nil { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s is nil", fieldName), + ) + } + if pkg.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s identifier is empty", fieldName), + ) + } + data, err := buildTaggedTBTCSignerDecodeHexField( + operation, + fmt.Sprintf("%s data", fieldName), + pkg.PackageHex, + ) + if err != nil { + return nil, err + } + + return &NativeFROSTDKGRound1Package{ + Identifier: pkg.Identifier, + Data: data, + }, nil +} + +func decodeBuildTaggedTBTCSignerDKGRound2Package( + operation string, + fieldName string, + pkg *buildTaggedTBTCSignerDKGRound2Package, + requireSenderIdentifier bool, +) (*NativeFROSTDKGRound2Package, error) { + if pkg == nil { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s is nil", fieldName), + ) + } + if pkg.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s identifier is empty", fieldName), + ) + } + if requireSenderIdentifier && (pkg.SenderIdentifier == nil || *pkg.SenderIdentifier == "") { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s sender identifier is empty", fieldName), + ) + } + data, err := buildTaggedTBTCSignerDecodeHexField( + operation, + fmt.Sprintf("%s data", fieldName), + pkg.PackageHex, + ) + if err != nil { + return nil, err + } + + senderIdentifier := "" + if pkg.SenderIdentifier != nil { + senderIdentifier = *pkg.SenderIdentifier + } + + return &NativeFROSTDKGRound2Package{ + Identifier: pkg.Identifier, + SenderIdentifier: senderIdentifier, + Data: data, + }, nil +} + +func decodeBuildTaggedTBTCSignerCommitment( + operation string, + fieldName string, + commitment *buildTaggedTBTCSignerNativeFROSTCommitment, +) (*NativeFROSTCommitment, error) { + if commitment == nil { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s is nil", fieldName), + ) + } + if commitment.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s identifier is empty", fieldName), + ) + } + data, err := buildTaggedTBTCSignerDecodeHexField( + operation, + fmt.Sprintf("%s data", fieldName), + commitment.DataHex, + ) + if err != nil { + return nil, err + } + + return &NativeFROSTCommitment{ + Identifier: commitment.Identifier, + Data: data, + }, nil +} + +func decodeBuildTaggedTBTCSignerSignatureShare( + operation string, + fieldName string, + signatureShare *buildTaggedTBTCSignerNativeFROSTSignatureShare, +) (*NativeFROSTSignatureShare, error) { + if signatureShare == nil { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s is nil", fieldName), + ) + } + if signatureShare.Identifier == "" { + return nil, buildTaggedTBTCSignerOperationError( + operation, + fmt.Sprintf("%s identifier is empty", fieldName), + ) + } + data, err := buildTaggedTBTCSignerDecodeHexField( + operation, + fmt.Sprintf("%s data", fieldName), + signatureShare.DataHex, + ) + if err != nil { + return nil, err + } + + return &NativeFROSTSignatureShare{ + Identifier: signatureShare.Identifier, + Data: data, + }, nil +} + +func appendBuildTaggedTBTCSignerStringMap( + source map[string]string, +) map[string]string { + if source == nil { + return nil + } + + copy := make(map[string]string, len(source)) + for key, value := range source { + copy[key] = value + } + + return copy +} + +func buildTaggedTBTCSignerStartSignRoundRequestPayload( + sessionID string, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, + taprootMerkleRoot *[32]byte, +) ([]byte, error) { + if sessionID == "" { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "session ID is empty", + ) + } + + if keyGroup == "" { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "key group is empty", + ) + } + + if memberIdentifier == 0 { + return nil, buildTaggedTBTCSignerOperationError( + "StartSignRound", + "member identifier is zero", + ) + } + + 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{}{} + } + + var taprootMerkleRootHex *string + if taprootMerkleRoot != nil { + encodedTaprootMerkleRoot := hex.EncodeToString(taprootMerkleRoot[:]) + taprootMerkleRootHex = &encodedTaprootMerkleRoot + } + + request := buildTaggedTBTCSignerStartSignRoundRequest{ + SessionID: sessionID, + MemberIdentifier: memberIdentifier, + MessageHex: hex.EncodeToString(message), + KeyGroup: keyGroup, + TaprootMerkleRootHex: taprootMerkleRootHex, + SigningParticipants: append([]uint16{}, signingParticipants...), + } + + 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", + ) + } + + 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 { + 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, + SigningParticipants: append([]uint16{}, response.SigningParticipants...), + OwnContribution: ownContribution, + }, nil +} + +func buildTaggedTBTCSignerFinalizeSignRoundRequestPayload( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, + taprootMerkleRoot *[32]byte, +) ([]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), + }, + ) + } + + var taprootMerkleRootHex *string + if taprootMerkleRoot != nil { + encodedTaprootMerkleRoot := hex.EncodeToString(taprootMerkleRoot[:]) + taprootMerkleRootHex = &encodedTaprootMerkleRoot + } + + request := buildTaggedTBTCSignerFinalizeSignRoundRequest{ + SessionID: sessionID, + TaprootMerkleRootHex: taprootMerkleRootHex, + 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 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) +} + +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 callBuildTaggedTBTCSignerDKGPart1( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "DKGPart1", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_dkg_part1(requestPtr, requestLen) + }, + ) +} + +func callBuildTaggedTBTCSignerDKGPart2( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "DKGPart2", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_dkg_part2(requestPtr, requestLen) + }, + ) +} + +func callBuildTaggedTBTCSignerDKGPart3( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "DKGPart3", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_dkg_part3(requestPtr, requestLen) + }, + ) +} + +func callBuildTaggedTBTCSignerGenerateNoncesAndCommitments( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "GenerateNoncesAndCommitments", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_generate_nonces_and_commitments( + requestPtr, + requestLen, + ) + }, + ) +} + +func callBuildTaggedTBTCSignerNewSigningPackage( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "NewSigningPackage", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_new_signing_package(requestPtr, requestLen) + }, + ) +} + +func callBuildTaggedTBTCSignerSignShare( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "SignShare", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_sign_share(requestPtr, requestLen) + }, + ) +} + +func callBuildTaggedTBTCSignerAggregate( + requestPayload []byte, +) ([]byte, error) { + return callBuildTaggedTBTCSignerOperation( + "Aggregate", + requestPayload, + func(requestPtr *C.uint8_t, requestLen C.size_t) C.TbtcSignerResult { + return C.tbtc_signer_aggregate(requestPtr, requestLen) + }, + ) +} + +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 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, + 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) { + // The C wrapper guards against a missing `frost_tbtc_free_buffer` symbol + // but not against a NULL buffer pointer. Status code -1 paths (FFI lib + // unavailable) and any future path that returns an empty buffer can leave + // `result.buffer.ptr == nil`, so skip the deferred free in that case to + // avoid handing a NULL pointer to Rust's `frost_tbtc_free_buffer`. + if result.buffer.ptr != nil { + defer C.tbtc_signer_free_buffer(result.buffer.ptr, result.buffer.len) + } + + statusCode := int32(result.status_code) + + 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)) + } + + statusErr := buildTaggedTBTCSignerResultStatusError(operation, statusCode, payload) + if statusErr != nil { + return nil, statusErr + } + + if len(payload) == 0 { + return nil, buildTaggedTBTCSignerOperationError( + operation, + "response payload is empty", + ) + } + + return payload, nil +} + +func buildTaggedTBTCSignerResultStatusError( + operation string, + statusCode int32, + payload []byte, +) error { + if statusCode == buildTaggedTBTCSignerUnavailableStatusCode { + return buildTaggedTBTCSignerUnavailableError(operation) + } + + if statusCode != 0 { + structured := buildTaggedTBTCSignerErrorPayload(payload) + return fmt.Errorf( + "%w: tbtc-signer bridge operation [%v] failed: [%w]", + ErrNativeBridgeOperationFailed, + operation, + structured, + ) + } + + return 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 new file mode 100644 index 0000000000..34bad66881 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native_test.go @@ -0,0 +1,1294 @@ +//go:build frost_native && frost_tbtc_signer && cgo + +package signing + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" + "testing" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" +) + +func TestRegisterBuildTaggedTBTCSignerEngine(t *testing.T) { + UnregisterNativeTBTCSignerEngine() + t.Cleanup(UnregisterNativeTBTCSignerEngine) + + err := registerBuildTaggedNativeFROSTSigningEngine() + if err != nil { + t.Fatalf("unexpected registration error: [%v]", err) + } + + engine := currentNativeTBTCSignerEngine() + if engine == nil { + t.Fatal("expected native tbtc-signer engine registration") + } + + _, err = engine.StartSignRound( + "session-1", + 1, + []byte("message"), + "key-group", + nil, + nil, + ) + if err == nil { + t.Fatal("expected unavailable tbtc-signer bridge error") + } + + 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) + } + + _, 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) + }) + 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 TestBuildTaggedTBTCSignerInteractiveFROSTBridge_WithLinkedSigner(t *testing.T) { + t.Setenv("TBTC_SIGNER_PROFILE", "development") + t.Setenv("TBTC_SIGNER_ENFORCE_PROVENANCE_GATE", "false") + + engine := &buildTaggedTBTCSignerEngine{} + participantIDs := []byte{1, 2, 3} + participantIdentifiers := make(map[byte]string, len(participantIDs)) + for _, participantID := range participantIDs { + participantIdentifiers[participantID] = buildTaggedTBTCSignerTestIdentifier( + participantID, + ) + } + + part1Results := make(map[byte]*NativeFROSTDKGPart1Result, len(participantIDs)) + for _, participantID := range participantIDs { + result, err := engine.Part1( + participantIdentifiers[participantID], + 3, + 2, + ) + if err != nil { + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Skip("linked tbtc-signer FFI symbols unavailable") + } + t.Fatalf("unexpected DKG part1 error: [%v]", err) + } + if result.Package.Identifier != participantIdentifiers[participantID] { + t.Fatalf("unexpected DKG part1 identifier: [%s]", result.Package.Identifier) + } + part1Results[participantID] = result + } + + part2Results := make(map[byte]*NativeFROSTDKGPart2Result, len(participantIDs)) + for _, participantID := range participantIDs { + round1Packages := make([]*NativeFROSTDKGRound1Package, 0, 2) + for _, otherParticipantID := range participantIDs { + if otherParticipantID == participantID { + continue + } + round1Packages = append( + round1Packages, + part1Results[otherParticipantID].Package, + ) + } + + result, err := engine.Part2( + part1Results[participantID].SecretPackage, + round1Packages, + ) + if err != nil { + t.Fatalf("unexpected DKG part2 error: [%v]", err) + } + if len(result.Packages) != 2 { + t.Fatalf("unexpected DKG part2 package count: [%d]", len(result.Packages)) + } + part2Results[participantID] = result + } + + part3Results := make(map[byte]*NativeFROSTDKGResult, len(participantIDs)) + for _, participantID := range participantIDs { + round1Packages := make([]*NativeFROSTDKGRound1Package, 0, 2) + for _, otherParticipantID := range participantIDs { + if otherParticipantID == participantID { + continue + } + round1Packages = append( + round1Packages, + part1Results[otherParticipantID].Package, + ) + } + + round2Packages := make([]*NativeFROSTDKGRound2Package, 0, 2) + for _, senderParticipantID := range participantIDs { + if senderParticipantID == participantID { + continue + } + var packageForRecipient *NativeFROSTDKGRound2Package + for _, pkg := range part2Results[senderParticipantID].Packages { + if pkg.Identifier == participantIdentifiers[participantID] { + packageForRecipient = pkg + break + } + } + if packageForRecipient == nil { + t.Fatalf( + "missing DKG round2 package from [%d] to [%d]", + senderParticipantID, + participantID, + ) + } + copied := *packageForRecipient + copied.SenderIdentifier = participantIdentifiers[senderParticipantID] + round2Packages = append(round2Packages, &copied) + } + + result, err := engine.Part3( + part2Results[participantID].SecretPackage, + round1Packages, + round2Packages, + ) + if err != nil { + t.Fatalf("unexpected DKG part3 error: [%v]", err) + } + if result.KeyPackage.Identifier != participantIdentifiers[participantID] { + t.Fatalf("unexpected DKG key package identifier") + } + if len(result.PublicKeyPackage.VerifyingKey) != 64 { + t.Fatalf( + "unexpected DKG x-only verifying key length: [%d]", + len(result.PublicKeyPackage.VerifyingKey), + ) + } + if len(result.PublicKeyPackage.VerifyingShares) != 3 { + t.Fatalf( + "unexpected DKG verifying share count: [%d]", + len(result.PublicKeyPackage.VerifyingShares), + ) + } + part3Results[participantID] = result + } + + verifyingKey := part3Results[1].PublicKeyPackage.VerifyingKey + for _, participantID := range participantIDs { + if part3Results[participantID].PublicKeyPackage.VerifyingKey != verifyingKey { + t.Fatal("DKG participants produced different group verifying keys") + } + } + + signingParticipants := []byte{1, 2} + commitments := make([]nativeFROSTCommitment, 0, len(signingParticipants)) + noncesByParticipant := make(map[byte][]byte, len(signingParticipants)) + for _, participantID := range signingParticipants { + nonces, commitmentIdentifier, commitmentData, err := + engine.GenerateNoncesAndCommitments( + part3Results[participantID].KeyPackage.Identifier, + part3Results[participantID].KeyPackage.Data, + ) + if err != nil { + t.Fatalf("unexpected nonce generation error: [%v]", err) + } + commitments = append(commitments, nativeFROSTCommitment{ + Identifier: commitmentIdentifier, + Data: commitmentData, + }) + noncesByParticipant[participantID] = nonces + } + + message := bytesOf(0x42, 32) + signingPackage, err := engine.NewSigningPackage(message, commitments) + if err != nil { + t.Fatalf("unexpected signing package error: [%v]", err) + } + + signatureShares := make( + []nativeFROSTSignatureShare, + 0, + len(signingParticipants), + ) + for _, participantID := range signingParticipants { + signatureShareIdentifier, signatureShareData, err := engine.Sign( + signingPackage, + noncesByParticipant[participantID], + part3Results[participantID].KeyPackage.Identifier, + part3Results[participantID].KeyPackage.Data, + ) + if err != nil { + t.Fatalf("unexpected signature share error: [%v]", err) + } + signatureShares = append(signatureShares, nativeFROSTSignatureShare{ + Identifier: signatureShareIdentifier, + Data: signatureShareData, + }) + } + + signatureBytes, err := engine.Aggregate( + signingPackage, + signatureShares, + part3Results[1].PublicKeyPackage, + ) + if err != nil { + t.Fatalf("unexpected aggregate error: [%v]", err) + } + if len(signatureBytes) != 64 { + t.Fatalf("unexpected aggregate signature length: [%d]", len(signatureBytes)) + } + + publicKeyBytes, err := hex.DecodeString(verifyingKey) + if err != nil { + t.Fatalf("cannot decode verifying key: [%v]", err) + } + publicKey, err := schnorr.ParsePubKey(publicKeyBytes) + if err != nil { + t.Fatalf("cannot parse verifying key: [%v]", err) + } + signature, err := schnorr.ParseSignature(signatureBytes) + if err != nil { + t.Fatalf("cannot parse aggregate signature: [%v]", err) + } + if !signature.Verify(message, publicKey) { + t.Fatal("aggregate signature does not verify under DKG x-only key") + } +} + +func buildTaggedTBTCSignerTestIdentifier(memberIndex byte) string { + identifier := make([]byte, 32) + identifier[0] = memberIndex + return fmt.Sprintf("%q", hex.EncodeToString(identifier)) +} + +func bytesOf(value byte, length int) []byte { + bytes := make([]byte, length) + for i := range bytes { + bytes[i] = value + } + return bytes +} + +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 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", + []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, + ) + } + + if request.DKGSeedHex != nil { + t.Fatalf("unexpected DKG seed hex: [%v]", *request.DKGSeedHex) + } +} + +func TestBuildTaggedTBTCSignerRunDKGRequestPayloadWithSeed(t *testing.T) { + payload, err := buildTaggedTBTCSignerRunDKGRequestPayloadWithSeed( + "session-1", + []NativeTBTCSignerDKGParticipant{ + { + Identifier: 1, + PublicKeyHex: "02aa", + }, + { + Identifier: 2, + PublicKeyHex: "02bb", + }, + }, + 2, + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + ) + 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.DKGSeedHex == nil { + t.Fatal("expected DKG seed hex") + } + if *request.DKGSeedHex != + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" { + t.Fatalf("unexpected DKG seed hex: [%v]", *request.DKGSeedHex) + } +} + +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, 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, + ) + } + }) + } +} + +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", + 3, + []byte{0xab, 0xcd}, + "key-group-1", + []uint16{1, 2, 3}, + nil, + ) + 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, + ) + } + + if request.MemberIdentifier != 3 { + t.Fatalf( + "unexpected member identifier\nexpected: [%v]\nactual: [%v]", + 3, + 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_TaprootMerkleRoot( + t *testing.T, +) { + var taprootMerkleRoot [32]byte + taprootMerkleRoot[0] = 0xab + taprootMerkleRoot[31] = 0xcd + + payload, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( + "session-1", + 3, + []byte{0xab, 0xcd}, + "key-group-1", + []uint16{1, 2, 3}, + &taprootMerkleRoot, + ) + 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.TaprootMerkleRootHex == nil { + t.Fatal("expected taproot merkle root") + } + + expectedTaprootMerkleRootHex := hex.EncodeToString(taprootMerkleRoot[:]) + if *request.TaprootMerkleRootHex != expectedTaprootMerkleRootHex { + t.Fatalf( + "unexpected taproot merkle root\nexpected: [%v]\nactual: [%v]", + expectedTaprootMerkleRootHex, + *request.TaprootMerkleRootHex, + ) + } +} + +func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_EmptySessionID(t *testing.T) { + _, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( + "", + 1, + []byte{0xab}, + "key-group-1", + nil, + nil, + ) + 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, + ) + } +} + +func TestBuildTaggedTBTCSignerStartSignRoundRequestPayload_ZeroMemberID(t *testing.T) { + _, err := buildTaggedTBTCSignerStartSignRoundRequestPayload( + "session-1", + 0, + []byte{0xab}, + "key-group-1", + nil, + nil, + ) + 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, + ) + } +} + +func TestBuildTaggedTBTCSignerFinalizeSignRoundRequestPayload(t *testing.T) { + payload, err := buildTaggedTBTCSignerFinalizeSignRoundRequestPayload( + "session-1", + []NativeTBTCSignerRoundContribution{ + { + Identifier: 7, + Data: []byte{0xde, 0xad}, + }, + }, + nil, + ) + 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 TestBuildTaggedTBTCSignerFinalizeSignRoundRequestPayload_TaprootMerkleRoot( + t *testing.T, +) { + var taprootMerkleRoot [32]byte + taprootMerkleRoot[0] = 0xab + taprootMerkleRoot[31] = 0xcd + + payload, err := buildTaggedTBTCSignerFinalizeSignRoundRequestPayload( + "session-1", + []NativeTBTCSignerRoundContribution{ + { + Identifier: 7, + Data: []byte{0xde, 0xad}, + }, + }, + &taprootMerkleRoot, + ) + 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.TaprootMerkleRootHex == nil { + t.Fatal("expected taproot merkle root") + } + + expectedTaprootMerkleRootHex := hex.EncodeToString(taprootMerkleRoot[:]) + if *request.TaprootMerkleRootHex != expectedTaprootMerkleRootHex { + t.Fatalf( + "unexpected taproot merkle root\nexpected: [%v]\nactual: [%v]", + expectedTaprootMerkleRootHex, + *request.TaprootMerkleRootHex, + ) + } +} + +func TestDecodeBuildTaggedTBTCSignerStartSignRoundResponse(t *testing.T) { + roundState, 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":3,"signature_share_hex":"deadbeef"}}`, + ), + ) + 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, + ) + } + + 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") + } + + 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 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, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", + 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, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", + 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, ErrNativeBridgeOperationFailed) { + t.Fatalf( + "unexpected error\nexpected: [%v]\nactual: [%v]", + ErrNativeBridgeOperationFailed, + err, + ) + } + + if errors.Is(err, ErrNativeCryptographyUnavailable) { + t.Fatalf( + "did not expect native cryptography unavailable error: [%v]", + err, + ) + } +} + +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], + ) + } + } +} + +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, 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, + ) + } + }) + } +} + +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_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..f6156db084 --- /dev/null +++ b/pkg/frost/signing/native_frost_engine_uniffi_registration_frost_native_default.go @@ -0,0 +1,7 @@ +//go:build frost_native && !(frost_tbtc_signer && cgo) + +package signing + +func registerBuildTaggedNativeFROSTSigningEngine() error { + return nil +} 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, + ) + } +} 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_coarse_signature_telemetry.go b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go new file mode 100644 index 0000000000..d406baed0c --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry.go @@ -0,0 +1,72 @@ +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. +// Only a single observer is supported. +func RegisterNativeTBTCSignerCoarseSignatureObserver( + observer NativeTBTCSignerCoarseSignatureObserver, +) error { + if observer == nil { + return fmt.Errorf("native tbtc-signer coarse signature observer is nil") + } + + nativeTBTCSignerCoarseSignatureObserverMutex.Lock() + defer nativeTBTCSignerCoarseSignatureObserverMutex.Unlock() + + if nativeTBTCSignerCoarseSignatureObserver != nil { + return fmt.Errorf( + "native tbtc-signer coarse signature observer is already registered", + ) + } + + 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..5c59d3a020 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_coarse_signature_telemetry_test.go @@ -0,0 +1,85 @@ +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 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) + + 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, + ) + } +} + +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", + }, + ) +} 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..cfecec5c53 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native.go @@ -0,0 +1,134 @@ +//go:build frost_native + +package signing + +import "fmt" + +// 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 { + Identifier uint16 `json:"identifier"` + 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 { + 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 +// operations. +type NativeTBTCSignerEngine interface { + RunDKG( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + ) (*NativeTBTCSignerDKGResult, error) + StartSignRound( + sessionID string, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, + taprootMerkleRoot *[32]byte, + ) (*NativeTBTCSignerRoundState, error) + FinalizeSignRound( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, + taprootMerkleRoot *[32]byte, + ) ([]byte, error) + BuildTaprootTx( + sessionID string, + inputs []NativeTBTCSignerTxInput, + outputs []NativeTBTCSignerTxOutput, + scriptTreeHex *string, + ) (*NativeTBTCSignerTxResult, error) +} + +// NativeTBTCSignerSeededDKGEngine is implemented by tbtc-signer engines that +// can pin development dealer DKG to an externally supplied seed. Production +// distributed DKG does not rely on this helper. +type NativeTBTCSignerSeededDKGEngine interface { + RunDKGWithSeed( + sessionID string, + participants []NativeTBTCSignerDKGParticipant, + threshold uint16, + dkgSeedHex string, + ) (*NativeTBTCSignerDKGResult, 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 +} + +// CurrentNativeTBTCSignerEngine returns the registered coarse tbtc-signer +// engine. +func CurrentNativeTBTCSignerEngine() NativeTBTCSignerEngine { + return currentNativeTBTCSignerEngine() +} + +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..06850fd530 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_engine_frost_native_test.go @@ -0,0 +1,91 @@ +//go:build frost_native + +package signing + +import ( + "fmt" + "testing" +) + +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, + memberIdentifier uint16, + message []byte, + keyGroup string, + signingParticipants []uint16, + taprootMerkleRoot *[32]byte, +) (*NativeTBTCSignerRoundState, error) { + _ = memberIdentifier + _ = signingParticipants + _ = taprootMerkleRoot + return nil, fmt.Errorf("not implemented") +} + +func (mntse *mockNativeTBTCSignerEngine) FinalizeSignRound( + sessionID string, + roundContributions []NativeTBTCSignerRoundContribution, + taprootMerkleRoot *[32]byte, +) ([]byte, error) { + _ = taprootMerkleRoot + 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) + + 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") + } +} diff --git a/pkg/frost/signing/native_tbtc_signer_error_frost_native.go b/pkg/frost/signing/native_tbtc_signer_error_frost_native.go new file mode 100644 index 0000000000..4e7e845120 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_error_frost_native.go @@ -0,0 +1,64 @@ +//go:build frost_native + +package signing + +import ( + "encoding/json" + "fmt" +) + +type buildTaggedTBTCSignerErrorResponse struct { + Code string `json:"code"` + Message string `json:"message"` +} + +// buildTaggedTBTCSignerStructuredError carries the FFI error envelope's +// structured fields so callers can match on Code via `errors.As` rather than +// substring-matching the rendered error string. Older signer builds may +// return errors without a Code field; this type still wraps them via the +// Message field, and consumers should treat an empty Code as a fall-back +// signal to apply legacy substring matching. +type buildTaggedTBTCSignerStructuredError struct { + Code string + Message string +} + +func (e *buildTaggedTBTCSignerStructuredError) Error() string { + if e == nil { + return "" + } + if e.Code != "" { + return fmt.Sprintf("%s: %s", e.Code, e.Message) + } + return e.Message +} + +// buildTaggedTBTCSignerErrorPayload decodes the FFI error envelope into a +// structured form so callers can match on the `Code` field via `errors.As` +// rather than rely on substring matching against the rendered error string. +// Decode failures and missing-fields edge cases are surfaced via the +// `Message` field with `Code` left empty so consumers know to fall back to +// legacy matching. +func buildTaggedTBTCSignerErrorPayload(payload []byte) *buildTaggedTBTCSignerStructuredError { + var errorResponse buildTaggedTBTCSignerErrorResponse + if err := json.Unmarshal(payload, &errorResponse); err != nil { + return &buildTaggedTBTCSignerStructuredError{ + Message: fmt.Sprintf( + "cannot decode error payload [%x]: %v", + payload, + err, + ), + } + } + + if errorResponse.Code == "" && errorResponse.Message == "" { + return &buildTaggedTBTCSignerStructuredError{ + Message: fmt.Sprintf("empty error payload: [%s]", string(payload)), + } + } + + return &buildTaggedTBTCSignerStructuredError{ + Code: errorResponse.Code, + Message: errorResponse.Message, + } +} 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..82a1469ffa --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry.go @@ -0,0 +1,66 @@ +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. +// Only a single observer is supported. +func RegisterNativeTBTCSignerFallbackObserver( + observer NativeTBTCSignerFallbackObserver, +) error { + if observer == nil { + return fmt.Errorf("native tbtc-signer fallback observer is nil") + } + + nativeTBTCSignerFallbackObserverMutex.Lock() + defer nativeTBTCSignerFallbackObserverMutex.Unlock() + + if nativeTBTCSignerFallbackObserver != nil { + return fmt.Errorf("native tbtc-signer fallback observer is already registered") + } + + 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..457b9710d2 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_fallback_telemetry_test.go @@ -0,0 +1,75 @@ +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 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) + + 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/frost/signing/native_tbtc_signer_material.go b/pkg/frost/signing/native_tbtc_signer_material.go new file mode 100644 index 0000000000..6e0f458630 --- /dev/null +++ b/pkg/frost/signing/native_tbtc_signer_material.go @@ -0,0 +1,71 @@ +package signing + +import ( + "os" + "strings" +) + +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. Material built + // with this source is placeholder data, not the output of a real FROST DKG + // run, and is refused by default at signing time. See + // `AcceptScaffoldKeyGroupEnvVar` for the opt-in escape hatch. + NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey = "legacy-wallet-pubkey" + // NativeTBTCSignerKeyGroupSourceDKGPersisted marks key-group material + // produced by a FROST wallet DKG and persisted for later signing. + NativeTBTCSignerKeyGroupSourceDKGPersisted = "dkg-persisted" + + // AcceptScaffoldKeyGroupEnvVar is the operator-facing opt-in that allows + // the FROST tbtc-signer FFI path to accept signer material whose + // `KeyGroupSource` is `legacy-wallet-pubkey`. Production deployments must + // not set this; it exists for local dev, CI, and integration rehearsals + // where a real DKG hand-off is not yet wired. + AcceptScaffoldKeyGroupEnvVar = "KEEP_CORE_FROST_TBTC_SIGNER_ACCEPT_SCAFFOLD_KEY_GROUP" +) + +// NativeTBTCSignerMaterialPayload is the signer-material payload schema for +// `frost-tbtc-signer-v1`. +type NativeTBTCSignerMaterialPayload struct { + KeyGroup string `json:"keyGroup"` + TaprootOutputKey string `json:"taprootOutputKey,omitempty"` + KeyGroupSource string `json:"keyGroupSource,omitempty"` + DKGSeedHex string `json:"dkgSeedHex,omitempty"` + DKGParticipants []NativeTBTCSignerDKGParticipant `json:"dkgParticipants,omitempty"` + DKGThreshold uint16 `json:"dkgThreshold,omitempty"` + 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"` +} + +// AcceptScaffoldKeyGroupEnabled reports whether the operator has opted into +// accepting scaffold-era (legacy-wallet-pubkey) key-group material. Without +// this, the signer material resolver and the FFI signing primitive both +// refuse legacy material rather than silently signing with placeholder +// cryptographic context. +// +// The env var is parsed identically to the bootstrap-mode flag in +// `pkg/frost/signing/backend.go`: case-insensitive `1`, `true`, `yes`, or +// `on`. Anything else (including missing/empty) is treated as disabled, so +// the safe-by-default behavior is to refuse. +func AcceptScaffoldKeyGroupEnabled() bool { + raw, ok := os.LookupEnv(AcceptScaffoldKeyGroupEnvVar) + if !ok { + return false + } + + switch strings.ToLower(strings.TrimSpace(raw)) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} diff --git a/pkg/frost/signing/request.go b/pkg/frost/signing/request.go new file mode 100644 index 0000000000..a9782593d5 --- /dev/null +++ b/pkg/frost/signing/request.go @@ -0,0 +1,64 @@ +package signing + +import ( + "fmt" + "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 + // 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 + // TaprootMerkleRoot carries the optional BIP-341 script merkle root used + // to tweak a Taproot key-path signature. + TaprootMerkleRoot *[32]byte + GroupSize int + DishonestThreshold int + Channel net.BroadcastChannel + 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/result.go b/pkg/frost/signing/result.go new file mode 100644 index 0000000000..bff53d34b4 --- /dev/null +++ b/pkg/frost/signing/result.go @@ -0,0 +1,11 @@ +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 + // Attempt contains execution metadata for the attempt producing Signature. + Attempt *Attempt +} diff --git a/pkg/frost/signing/roast_retry_attempt_handle_default_build.go b/pkg/frost/signing/roast_retry_attempt_handle_default_build.go new file mode 100644 index 0000000000..8bc28b14da --- /dev/null +++ b/pkg/frost/signing/roast_retry_attempt_handle_default_build.go @@ -0,0 +1,50 @@ +//go:build !frost_roast_retry + +package signing + +import ( + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" +) + +// SetCurrentAttemptHandleForSession is a no-op in the default build: +// the receive loops will never find a handle for any session, so the +// snapshot submission path is dormant. The build-tagged +// implementation does the real registration. +func SetCurrentAttemptHandleForSession( + _ string, + _ roast.AttemptHandle, + _ attempt.AttemptContext, +) { +} + +// ClearCurrentAttemptHandleForSession is a no-op in the default +// build. +func ClearCurrentAttemptHandleForSession(_ string) {} + +// ResetSessionHandleRegistryForTest is a no-op in the default +// build. +func ResetSessionHandleRegistryForTest() {} + +// StartSessionHandleSweeper is a no-op in the default build: with +// no real registry there is nothing to sweep. +func StartSessionHandleSweeper() {} + +// currentAttemptHandleForCollect always returns ok=false in the +// default build, so submitSnapshotIfActive exits without attempting +// the RecordEvidence call. +func currentAttemptHandleForCollect( + _ string, +) (roast.AttemptHandle, attempt.AttemptContext, bool) { + return roast.AttemptHandle{}, attempt.AttemptContext{}, false +} + +// CurrentAttemptHandleForSession is the exported alias for +// callers outside the package (e.g. the ROAST-driven signing +// selector in pkg/tbtc). In the default build it is a no-op that +// always returns ok=false. +func CurrentAttemptHandleForSession( + sessionID string, +) (roast.AttemptHandle, attempt.AttemptContext, bool) { + return currentAttemptHandleForCollect(sessionID) +} diff --git a/pkg/frost/signing/roast_retry_attempt_handle_frost_roast_retry.go b/pkg/frost/signing/roast_retry_attempt_handle_frost_roast_retry.go new file mode 100644 index 0000000000..653a162b25 --- /dev/null +++ b/pkg/frost/signing/roast_retry_attempt_handle_frost_roast_retry.go @@ -0,0 +1,177 @@ +//go:build frost_roast_retry + +package signing + +import ( + "sync" + "time" + + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" +) + +// SessionHandleBindingTTL is the maximum age the eviction sweep +// tolerates for a sessionAttemptBinding before treating it as +// orphaned. The two-hour default is documented in RFC-21's +// Resolved decisions section: long enough that no real signing +// session reaches it, short enough that a leaked binding cannot +// accumulate across days of node uptime. +const SessionHandleBindingTTL = 2 * time.Hour + +// SessionHandleSweepInterval is how often the background sweeper +// goroutine wakes up to evict stale bindings. Coarse-grained on +// purpose: the sweep is a defence-in-depth backstop, not a tight +// liveness mechanism. 15 minutes balances responsiveness against +// goroutine churn. +const SessionHandleSweepInterval = 15 * time.Minute + +// sessionAttemptBinding records the current attempt's handle and +// context for a session. The orchestration layer (Phase 5+) sets +// the binding via SetCurrentAttemptHandleForSession before driving +// the round-one / round-two / contribution receive loops; the +// receive loops read it at end-of-collect to know which attempt to +// submit their evidence snapshot against. +// +// createdAt is the wall-clock time at which the binding was last +// (re)set. The background sweeper evicts bindings older than +// SessionHandleBindingTTL. +type sessionAttemptBinding struct { + handle roast.AttemptHandle + context attempt.AttemptContext + createdAt time.Time +} + +var ( + sessionAttemptBindingMu sync.RWMutex + sessionAttemptBindings = map[string]sessionAttemptBinding{} + + sweeperOnce sync.Once + sweeperStop chan struct{} +) + +// SetCurrentAttemptHandleForSession records the in-flight attempt +// handle and context for the named session. Callers in the +// orchestration layer (Phase 5+) invoke this immediately after +// Coordinator.BeginAttempt so receive loops can correlate their +// captured evidence with the right attempt. +// +// Later calls for the same session overwrite earlier ones (this is +// the documented behaviour: a session whose attempt has transitioned +// re-binds to the new attempt's handle). +// +// The binding's createdAt is set to the current wall-clock time so +// the background sweeper can evict it if Clear is never called +// (panic before the deferred clear, etc.). +func SetCurrentAttemptHandleForSession( + sessionID string, + handle roast.AttemptHandle, + ctx attempt.AttemptContext, +) { + sessionAttemptBindingMu.Lock() + defer sessionAttemptBindingMu.Unlock() + sessionAttemptBindings[sessionID] = sessionAttemptBinding{ + handle: handle, + context: ctx, + createdAt: time.Now(), + } +} + +// ClearCurrentAttemptHandleForSession removes any binding for the +// named session. Callers invoke this when a session terminates so +// the registry does not grow unbounded. +func ClearCurrentAttemptHandleForSession(sessionID string) { + sessionAttemptBindingMu.Lock() + defer sessionAttemptBindingMu.Unlock() + delete(sessionAttemptBindings, sessionID) +} + +// ResetSessionHandleRegistryForTest clears every binding and stops +// the background sweeper if one is running. Exposed only for +// tests; not for production code paths. +func ResetSessionHandleRegistryForTest() { + sessionAttemptBindingMu.Lock() + defer sessionAttemptBindingMu.Unlock() + sessionAttemptBindings = map[string]sessionAttemptBinding{} + if sweeperStop != nil { + close(sweeperStop) + sweeperStop = nil + sweeperOnce = sync.Once{} + } +} + +// StartSessionHandleSweeper launches the background goroutine that +// evicts sessionAttemptBindings older than SessionHandleBindingTTL. +// Idempotent via sync.Once: the first caller starts the sweeper; +// subsequent calls are no-ops. The sweeper runs for the lifetime of +// the process (until ResetSessionHandleRegistryForTest stops it, +// which only tests do). +// +// Phase 5.2 starts the sweeper from RegisterRoastRetryCoordinator +// so the defence-in-depth backstop is active whenever orchestration +// could plausibly run. +func StartSessionHandleSweeper() { + sweeperOnce.Do(func() { + sessionAttemptBindingMu.Lock() + sweeperStop = make(chan struct{}) + stop := sweeperStop + sessionAttemptBindingMu.Unlock() + go sessionHandleSweepLoop(stop) + }) +} + +func sessionHandleSweepLoop(stop <-chan struct{}) { + ticker := time.NewTicker(SessionHandleSweepInterval) + defer ticker.Stop() + for { + select { + case <-stop: + return + case <-ticker.C: + evictStaleSessionHandleBindings(SessionHandleBindingTTL) + } + } +} + +// evictStaleSessionHandleBindings sweeps the binding map and +// removes entries older than maxAge. Exposed at the package level +// so tests can invoke it directly with small maxAge values without +// waiting for the sweeper ticker. +func evictStaleSessionHandleBindings(maxAge time.Duration) int { + cutoff := time.Now().Add(-maxAge) + sessionAttemptBindingMu.Lock() + defer sessionAttemptBindingMu.Unlock() + evicted := 0 + for sessionID, binding := range sessionAttemptBindings { + if binding.createdAt.Before(cutoff) { + delete(sessionAttemptBindings, sessionID) + evicted++ + } + } + return evicted +} + +// currentAttemptHandleForCollect reads the binding the orchestration +// layer set for this session. Returns (zero, zero, false) when no +// binding exists -- the typical Phase-4 state, where no orchestration +// is wired yet. The submit helper takes ok=false as the signal to +// skip the RecordEvidence call. +func currentAttemptHandleForCollect( + sessionID string, +) (roast.AttemptHandle, attempt.AttemptContext, bool) { + sessionAttemptBindingMu.RLock() + defer sessionAttemptBindingMu.RUnlock() + binding, ok := sessionAttemptBindings[sessionID] + if !ok { + return roast.AttemptHandle{}, attempt.AttemptContext{}, false + } + return binding.handle, binding.context, true +} + +// CurrentAttemptHandleForSession is the exported alias for callers +// outside the package (e.g. the ROAST-driven signing selector in +// pkg/tbtc). It is identical to currentAttemptHandleForCollect. +func CurrentAttemptHandleForSession( + sessionID string, +) (roast.AttemptHandle, attempt.AttemptContext, bool) { + return currentAttemptHandleForCollect(sessionID) +} diff --git a/pkg/frost/signing/roast_retry_bundle_registry_default_build.go b/pkg/frost/signing/roast_retry_bundle_registry_default_build.go new file mode 100644 index 0000000000..35493ee5a0 --- /dev/null +++ b/pkg/frost/signing/roast_retry_bundle_registry_default_build.go @@ -0,0 +1,26 @@ +//go:build !frost_roast_retry + +package signing + +import "github.com/keep-network/keep-core/pkg/frost/roast" + +// RecordTransitionBundleForSession is a no-op in the default build: +// the per-session bundle registry is not active without the +// frost_roast_retry tag. The signing-loop ROAST selector (when +// installed via Phase 7's build) reads this registry to consume +// the most recent TransitionMessage for a message. +func RecordTransitionBundleForSession(_ string, _ *roast.TransitionMessage) {} + +// TransitionBundleForSession returns (nil, false) in the default +// build, signalling to callers that no ROAST bundle is available +// and the legacy retry shuffle should be used. +func TransitionBundleForSession(_ string) (*roast.TransitionMessage, bool) { + return nil, false +} + +// ClearTransitionBundleForSession is a no-op in the default build. +func ClearTransitionBundleForSession(_ string) {} + +// ResetTransitionBundleRegistryForTest is a no-op in the default +// build. +func ResetTransitionBundleRegistryForTest() {} diff --git a/pkg/frost/signing/roast_retry_bundle_registry_frost_roast_retry.go b/pkg/frost/signing/roast_retry_bundle_registry_frost_roast_retry.go new file mode 100644 index 0000000000..41bd306c86 --- /dev/null +++ b/pkg/frost/signing/roast_retry_bundle_registry_frost_roast_retry.go @@ -0,0 +1,105 @@ +//go:build frost_roast_retry + +package signing + +import ( + "sync" + "time" + + "github.com/keep-network/keep-core/pkg/frost/roast" +) + +// TransitionBundleRegistryTTL is how long a session's most recent +// TransitionMessage is retained before the background sweeper +// evicts it. Matches the session-handle TTL: a bundle's usefulness +// to retry-driven participant selection expires when the session +// it describes is itself archived. +const TransitionBundleRegistryTTL = SessionHandleBindingTTL + +// sessionBundleEntry pairs a TransitionMessage with the wall-clock +// time at which it was recorded so the sweeper can evict stale +// entries. +type sessionBundleEntry struct { + bundle *roast.TransitionMessage + createdAt time.Time +} + +var ( + sessionBundleRegistryMu sync.RWMutex + sessionBundleRegistry = map[string]sessionBundleEntry{} +) + +// RecordTransitionBundleForSession stores the most recent +// TransitionMessage produced by the elected coordinator for the +// named session. The bundle is later consumed by the ROAST-driven +// signingParticipantSelector to compute the next attempt's +// IncludedSet via EvaluateRoastRetryForSigning. +// +// A later call for the same session overwrites the earlier bundle +// -- the registry tracks only the most recent transition. +func RecordTransitionBundleForSession( + sessionID string, + bundle *roast.TransitionMessage, +) { + if bundle == nil { + return + } + sessionBundleRegistryMu.Lock() + defer sessionBundleRegistryMu.Unlock() + sessionBundleRegistry[sessionID] = sessionBundleEntry{ + bundle: bundle, + createdAt: time.Now(), + } +} + +// TransitionBundleForSession returns the most recent transition +// message for the named session, plus a presence flag. Callers +// (the ROAST selector) treat (nil, false) as "no bundle; fall back +// to legacy". +func TransitionBundleForSession( + sessionID string, +) (*roast.TransitionMessage, bool) { + sessionBundleRegistryMu.RLock() + defer sessionBundleRegistryMu.RUnlock() + entry, ok := sessionBundleRegistry[sessionID] + if !ok { + return nil, false + } + return entry.bundle, true +} + +// ClearTransitionBundleForSession removes any bundle for the named +// session. Called when a session terminates. +func ClearTransitionBundleForSession(sessionID string) { + sessionBundleRegistryMu.Lock() + defer sessionBundleRegistryMu.Unlock() + delete(sessionBundleRegistry, sessionID) +} + +// ResetTransitionBundleRegistryForTest clears every bundle. Test- +// only seam. +func ResetTransitionBundleRegistryForTest() { + sessionBundleRegistryMu.Lock() + defer sessionBundleRegistryMu.Unlock() + sessionBundleRegistry = map[string]sessionBundleEntry{} +} + +// evictStaleTransitionBundles sweeps the registry and removes +// entries older than maxAge. Exposed at the package level so +// tests can invoke it directly with small maxAge values. The +// production sweeper invokes it from sessionHandleSweepLoop +// (Phase 5.2) so the bundle and handle registries share a single +// background goroutine. +func evictStaleTransitionBundles(maxAge time.Duration) int { + cutoff := time.Now().Add(-maxAge) + sessionBundleRegistryMu.Lock() + defer sessionBundleRegistryMu.Unlock() + evicted := 0 + for sessionID, entry := range sessionBundleRegistry { + if entry.createdAt.Before(cutoff) { + delete(sessionBundleRegistry, sessionID) + evicted++ + } + } + return evicted +} diff --git a/pkg/frost/signing/roast_retry_bundle_registry_frost_roast_retry_test.go b/pkg/frost/signing/roast_retry_bundle_registry_frost_roast_retry_test.go new file mode 100644 index 0000000000..cca286467c --- /dev/null +++ b/pkg/frost/signing/roast_retry_bundle_registry_frost_roast_retry_test.go @@ -0,0 +1,109 @@ +//go:build frost_roast_retry + +package signing + +import ( + "testing" + "time" + + "github.com/keep-network/keep-core/pkg/frost/roast" +) + +func TestTransitionBundleRegistry_RoundTrip(t *testing.T) { + ResetTransitionBundleRegistryForTest() + t.Cleanup(ResetTransitionBundleRegistryForTest) + + bundle := &roast.TransitionMessage{ + CoordinatorIDValue: 7, + } + RecordTransitionBundleForSession("session-A", bundle) + + got, ok := TransitionBundleForSession("session-A") + if !ok { + t.Fatal("expected bundle to be present after Record") + } + if got.CoordinatorIDValue != 7 { + t.Fatalf( + "bundle round-trip mismatch: got coordinator %d, want 7", + got.CoordinatorIDValue, + ) + } +} + +func TestTransitionBundleRegistry_LaterRecordOverwrites(t *testing.T) { + ResetTransitionBundleRegistryForTest() + t.Cleanup(ResetTransitionBundleRegistryForTest) + + RecordTransitionBundleForSession("session-B", &roast.TransitionMessage{CoordinatorIDValue: 1}) + RecordTransitionBundleForSession("session-B", &roast.TransitionMessage{CoordinatorIDValue: 2}) + got, ok := TransitionBundleForSession("session-B") + if !ok { + t.Fatal("expected bundle to be present") + } + if got.CoordinatorIDValue != 2 { + t.Fatalf( + "later Record must overwrite earlier: got %d, want 2", + got.CoordinatorIDValue, + ) + } +} + +func TestTransitionBundleRegistry_ClearRemovesBundle(t *testing.T) { + ResetTransitionBundleRegistryForTest() + t.Cleanup(ResetTransitionBundleRegistryForTest) + + RecordTransitionBundleForSession("session-clear", &roast.TransitionMessage{}) + if _, ok := TransitionBundleForSession("session-clear"); !ok { + t.Fatal("setup: bundle must exist") + } + ClearTransitionBundleForSession("session-clear") + if _, ok := TransitionBundleForSession("session-clear"); ok { + t.Fatal("bundle must be removed after Clear") + } +} + +func TestTransitionBundleRegistry_NilBundleIsIgnored(t *testing.T) { + ResetTransitionBundleRegistryForTest() + t.Cleanup(ResetTransitionBundleRegistryForTest) + + RecordTransitionBundleForSession("session-nil", nil) + if _, ok := TransitionBundleForSession("session-nil"); ok { + t.Fatal("nil bundle must be discarded") + } +} + +func TestEvictStaleTransitionBundles_RemovesOldEntries(t *testing.T) { + ResetTransitionBundleRegistryForTest() + t.Cleanup(ResetTransitionBundleRegistryForTest) + + RecordTransitionBundleForSession("session-old", &roast.TransitionMessage{CoordinatorIDValue: 1}) + // Backdate. + sessionBundleRegistryMu.Lock() + entry := sessionBundleRegistry["session-old"] + entry.createdAt = time.Now().Add(-10 * time.Minute) + sessionBundleRegistry["session-old"] = entry + sessionBundleRegistryMu.Unlock() + + RecordTransitionBundleForSession("session-new", &roast.TransitionMessage{CoordinatorIDValue: 2}) + + evicted := evictStaleTransitionBundles(5 * time.Minute) + if evicted != 1 { + t.Fatalf("expected 1 eviction, got %d", evicted) + } + if _, ok := TransitionBundleForSession("session-old"); ok { + t.Fatal("old bundle must be evicted") + } + if _, ok := TransitionBundleForSession("session-new"); !ok { + t.Fatal("new bundle must survive") + } +} + +func TestTransitionBundleRegistryTTL_MatchesSessionHandleTTL(t *testing.T) { + if TransitionBundleRegistryTTL != SessionHandleBindingTTL { + t.Fatalf( + "bundle TTL %s != session-handle TTL %s; bundles must not outlive sessions", + TransitionBundleRegistryTTL, + SessionHandleBindingTTL, + ) + } +} diff --git a/pkg/frost/signing/roast_retry_bundle_registry_test.go b/pkg/frost/signing/roast_retry_bundle_registry_test.go new file mode 100644 index 0000000000..d0b1c6204a --- /dev/null +++ b/pkg/frost/signing/roast_retry_bundle_registry_test.go @@ -0,0 +1,34 @@ +//go:build !frost_roast_retry + +package signing + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast" +) + +func TestTransitionBundleRegistry_DefaultBuildIsNoOp(t *testing.T) { + // In the default build the registry is a permanent stub: + // RecordTransitionBundleForSession discards; TransitionBundleForSession + // always returns (nil, false). The ROAST selector must therefore + // always fall back to legacy retry in the default build. + RecordTransitionBundleForSession( + "session-default-build-test", + &roast.TransitionMessage{}, + ) + got, ok := TransitionBundleForSession("session-default-build-test") + if ok { + t.Fatalf( + "default build registry must report not-present; got bundle %v", + got, + ) + } + if got != nil { + t.Fatalf("default build must return nil bundle; got %v", got) + } + + // Clear and reset must not panic. + ClearTransitionBundleForSession("session-default-build-test") + ResetTransitionBundleRegistryForTest() +} diff --git a/pkg/frost/signing/roast_retry_executor_entry_default_build.go b/pkg/frost/signing/roast_retry_executor_entry_default_build.go new file mode 100644 index 0000000000..96e21f9ba5 --- /dev/null +++ b/pkg/frost/signing/roast_retry_executor_entry_default_build.go @@ -0,0 +1,26 @@ +//go:build !frost_native + +package signing + +import "github.com/ipfs/go-log/v2" + +// attemptRoastRetryOrchestrationFromRequest is the executor-adapter +// entry point for RFC-21 Phase-6 ROAST orchestration. In the +// default build (no frost_native tag) it is a permanent no-op +// stub: orchestration cannot run without the frost_native code +// path, so the executor adapter behaves exactly as in Phase 5. +// +// The function returns (cleanup, error). cleanup is non-nil when +// orchestration started successfully; the executor adapter defers +// it. error is non-nil only for RUNTIME failures the executor +// must propagate to its caller (static-configuration errors are +// logged and the cleanup is returned nil to signal "no +// orchestration; fall back to legacy receive-loop semantics"). +// +// The default-build stub returns (nil, nil) unconditionally. +func attemptRoastRetryOrchestrationFromRequest( + _ *NativeExecutionFFISigningRequest, + _ log.StandardLogger, +) (func(), error) { + return nil, nil +} diff --git a/pkg/frost/signing/roast_retry_executor_entry_frost_native.go b/pkg/frost/signing/roast_retry_executor_entry_frost_native.go new file mode 100644 index 0000000000..117434188d --- /dev/null +++ b/pkg/frost/signing/roast_retry_executor_entry_frost_native.go @@ -0,0 +1,102 @@ +//go:build frost_native + +package signing + +import ( + "errors" + "fmt" + + "github.com/ipfs/go-log/v2" +) + +// attemptRoastRetryOrchestrationFromRequest is the executor-adapter +// entry point for RFC-21 Phase-6 ROAST orchestration. It: +// +// 1. Builds an attempt.AttemptContext from the FFI signing +// request (BuildAttemptContextFromRequest, gated frost_native). +// +// 2. If construction fails with ErrUnsupportedSignerMaterialFormat +// -- e.g. the deployment still uses FrostUniFFIV1 material -- +// the failure is a STATIC configuration condition: every +// honest signer with the same deployment material observes the +// same error deterministically. Log at INFO and return +// (nil, nil) so the executor proceeds without orchestration. +// +// 3. Any other AttemptContext construction error is a RUNTIME +// failure (nil fields, malformed material payload, etc.). Per +// the RFC-21 Phase-6 orchestration error taxonomy, runtime +// errors must HARD FAIL to prevent group fracture: node A +// falling back to legacy while node B proceeds with ROAST +// would split the participant set on NextAttempt. +// +// 4. Calls BeginOrchestrationForSession with the context. +// ErrRoastRetryReadinessOptOut and +// ErrNoRoastRetryCoordinatorRegistered are static-configuration +// errors -- log at INFO and return (nil, nil). Any other error +// is treated as RUNTIME and propagated unchanged. +// +// 5. On success returns the cleanup function the executor adapter +// must defer. +// +// The function returns (cleanup, error): +// - cleanup non-nil + error nil -> orchestration active; defer cleanup. +// - cleanup nil + error nil -> static fallback; proceed legacy. +// - cleanup nil + error non-nil -> runtime failure; propagate. +func attemptRoastRetryOrchestrationFromRequest( + request *NativeExecutionFFISigningRequest, + logger log.StandardLogger, +) (func(), error) { + if logger == nil { + // Defensive: existing executor-adapter tests pass nil here. + // The helper logs static-fallback diagnostics, so a nil + // logger must not panic the executor. + logger = log.Logger("keep-frost-roast-orchestration") + } + ctx, err := BuildAttemptContextFromRequest(request) + if err != nil { + // All BuildAttemptContextFromRequest errors are treated as + // STATIC fallbacks because they are deterministic per-input: + // the same NativeExecutionFFISigningRequest produces the + // same construction outcome on every honest node, so + // every node would make the same fall-back decision. The + // RFC-21 Phase-6 hard-fail discipline applies only to + // non-deterministic RUNTIME errors that originate inside + // the Coordinator state machine (next branch). + logger.Infof( + "ROAST orchestration unavailable for session %q: %v", + request.SessionID, + err, + ) + return nil, nil + } + logger.Infof( + "ROAST signer-material telemetry: session=%q key_group_id=%q signer_material_format=%q", + request.SessionID, + ctx.KeyGroupID, + request.SignerMaterial.Format, + ) + + handle, cleanup, err := BeginOrchestrationForSession(request.SessionID, ctx) + if err != nil { + switch { + case errors.Is(err, ErrRoastRetryReadinessOptOut), + errors.Is(err, ErrNoRoastRetryCoordinatorRegistered): + // Static-configuration errors -> safe to fall back. + logger.Infof( + "ROAST retry disabled for session %q: %v", + request.SessionID, + err, + ) + return nil, nil + default: + // Runtime failure: HARD FAIL. + return nil, fmt.Errorf( + "ROAST orchestration: begin session %q: %w", + request.SessionID, + err, + ) + } + } + _ = handle // Phase 6.4+ uses this for retry adapter invocation. + return cleanup, nil +} diff --git a/pkg/frost/signing/roast_retry_executor_entry_frost_native_test.go b/pkg/frost/signing/roast_retry_executor_entry_frost_native_test.go new file mode 100644 index 0000000000..e96c95077a --- /dev/null +++ b/pkg/frost/signing/roast_retry_executor_entry_frost_native_test.go @@ -0,0 +1,146 @@ +//go:build frost_native + +package signing + +import ( + "encoding/json" + "fmt" + "math/big" + "strings" + "testing" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/internal/testutils" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func newEntryTestRequest(t *testing.T) *NativeExecutionFFISigningRequest { + t.Helper() + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: "tbtc-signer-entry-group", + }) + return &NativeExecutionFFISigningRequest{ + Message: new(big.Int).SetBytes([]byte{0xab, 0xcd}), + SessionID: "executor-entry-test", + MemberIndex: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + }, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2, 3, 4, 5}, + }, + } +} + +func TestEntry_StaticFallback_NoCoordinatorRegistered_TaggedBuild(t *testing.T) { + // Without the frost_roast_retry build tag this is exercised by + // the default-build test (which always falls through). Under the + // frost_native build alone, the helper still treats the absence + // of a registered coordinator as a static fallback because + // BeginOrchestrationForSession returns + // ErrNoRoastRetryCoordinatorRegistered (in the default build it + // is the stub no-op-return-true). + // + // The helper must return (nil, nil) regardless: the executor + // adapter proceeds without orchestration, matching Phase 5 + // receive semantics. + logger := log.Logger("entry-static-test") + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + newEntryTestRequest(t), logger, + ) + if err != nil { + t.Fatalf("static fallback must not surface an error: %v", err) + } + if cleanup != nil { + t.Fatal("static fallback must not return a cleanup function") + } +} + +func TestEntry_LogsSignerMaterialFormatTelemetry(t *testing.T) { + logger := &captureInfoLogger{} + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + newEntryTestRequest(t), logger, + ) + if err != nil { + t.Fatalf("static fallback must not surface an error: %v", err) + } + if cleanup != nil { + t.Fatal("static fallback must not return a cleanup function") + } + + joined := strings.Join(logger.infoMessages, "\n") + if !strings.Contains(joined, "signer_material_format") || + !strings.Contains(joined, NativeSignerMaterialFormatFrostTBTCSignerV1) || + !strings.Contains(joined, "key_group_id") { + t.Fatalf("missing signer-material telemetry in logs: [%s]", joined) + } +} + +func TestEntry_StaticFallback_UnsupportedSignerFormat(t *testing.T) { + // FrostUniFFIV1 material -> ExtractDkgGroupPublicKeyFromMaterial + // returns ErrUnsupportedSignerMaterialFormat. The helper must + // treat this as STATIC (deterministic across deployments) and + // fall back without surfacing an error. + req := newEntryTestRequest(t) + req.SignerMaterial = &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte("{}"), + } + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + req, log.Logger("entry-v1-test"), + ) + if err != nil { + t.Fatalf("V1 material must be a static fallback: %v", err) + } + if cleanup != nil { + t.Fatal("static fallback must not return a cleanup function") + } +} + +func TestEntry_StaticFallback_OnNilSignerMaterial(t *testing.T) { + // Nil signer material is a deterministic, per-input + // construction-precondition failure: every honest node with + // the same request would observe it identically. Treated as a + // STATIC fallback so the executor adapter proceeds without + // orchestration. The HARD-FAIL discipline is reserved for + // non-deterministic Coordinator state-machine errors. + req := newEntryTestRequest(t) + req.SignerMaterial = nil + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + req, log.Logger("entry-nil-mat-test"), + ) + if err != nil { + t.Fatalf("nil signer material must be a STATIC fallback; got %v", err) + } + if cleanup != nil { + t.Fatal("static fallback must not return cleanup") + } +} + +type captureInfoLogger struct { + testutils.MockLogger + infoMessages []string +} + +func (cil *captureInfoLogger) Infof(format string, args ...interface{}) { + cil.infoMessages = append(cil.infoMessages, fmt.Sprintf(format, args...)) +} + +func TestEntry_StaticFallback_OnZeroAttemptNumber(t *testing.T) { + // Zero attempt number is also a deterministic precondition + // failure; treated as STATIC fallback. + req := newEntryTestRequest(t) + req.Attempt.Number = 0 + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + req, log.Logger("entry-zero-attempt-test"), + ) + if err != nil { + t.Fatalf("zero attempt number must be a STATIC fallback; got %v", err) + } + if cleanup != nil { + t.Fatal("static fallback must not return cleanup") + } +} diff --git a/pkg/frost/signing/roast_retry_executor_entry_frost_roast_retry_test.go b/pkg/frost/signing/roast_retry_executor_entry_frost_roast_retry_test.go new file mode 100644 index 0000000000..bfb5d76631 --- /dev/null +++ b/pkg/frost/signing/roast_retry_executor_entry_frost_roast_retry_test.go @@ -0,0 +1,190 @@ +//go:build frost_native && frost_roast_retry + +package signing + +import ( + "encoding/json" + "errors" + "math/big" + "testing" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func newEntryRetryTestRequest(t *testing.T) *NativeExecutionFFISigningRequest { + t.Helper() + payload, _ := json.Marshal(&NativeTBTCSignerMaterialPayload{ + KeyGroup: "tbtc-signer-entry-retry-group", + }) + return &NativeExecutionFFISigningRequest{ + Message: new(big.Int).SetBytes([]byte{0xab, 0xcd}), + SessionID: "executor-entry-retry-test", + MemberIndex: 1, + SignerMaterial: &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + }, + Attempt: &Attempt{ + Number: 1, + CoordinatorMemberIndex: 1, + IncludedMembersIndexes: []group.MemberIndex{1, 2, 3, 4, 5}, + }, + } +} + +func TestEntry_StaticFallback_ReadinessOptInUnset(t *testing.T) { + // Explicitly unset the env var. + t.Setenv(RoastRetryReadinessOptInEnvVar, "") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + // Register a coordinator -- the env var alone keeps us in + // fallback. + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + newEntryRetryTestRequest(t), log.Logger("entry-no-optin"), + ) + if err != nil { + t.Fatalf("static fallback (env var unset) must not surface an error: %v", err) + } + if cleanup != nil { + t.Fatal("static fallback must not return a cleanup function") + } +} + +func TestEntry_StaticFallback_RegistryEmpty(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + // Registry is empty (no Register call). + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + newEntryRetryTestRequest(t), log.Logger("entry-no-registry"), + ) + if err != nil { + t.Fatalf("static fallback (registry empty) must not surface an error: %v", err) + } + if cleanup != nil { + t.Fatal("static fallback must not return a cleanup function") + } +} + +func TestEntry_HappyPath_ActivatesOrchestration(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + req := newEntryRetryTestRequest(t) + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + req, log.Logger("entry-happy"), + ) + if err != nil { + t.Fatalf("happy path must not error: %v", err) + } + if cleanup == nil { + t.Fatal("happy path must return a cleanup function") + } + + // Binding must exist for the session. + if _, _, ok := currentAttemptHandleForCollect(req.SessionID); !ok { + t.Fatal("binding must exist after orchestration entry") + } + cleanup() + if _, _, ok := currentAttemptHandleForCollect(req.SessionID); ok { + t.Fatal("binding must be cleared after cleanup") + } +} + +func TestEntry_HardFail_RuntimeBeginAttemptFailure(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + // Register an erroring coordinator -- BeginAttempt fails for + // runtime reasons. Per the RFC-21 taxonomy, this must HARD FAIL. + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: &erroringEntryCoordinator{ + err: errors.New("synthetic begin-attempt runtime failure"), + }, + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + newEntryRetryTestRequest(t), log.Logger("entry-hard-fail"), + ) + if err == nil { + t.Fatal("runtime BeginAttempt error must HARD FAIL (not static fallback)") + } + if cleanup != nil { + t.Fatal("hard-fail must not return cleanup") + } + if !contains(err.Error(), "synthetic begin-attempt runtime failure") { + t.Fatalf("error must propagate underlying cause; got %v", err) + } +} + +// erroringEntryCoordinator implements roast.Coordinator with a +// synthetic BeginAttempt failure. Used to verify the HARD-FAIL +// branch of the executor-adapter entry helper. +type erroringEntryCoordinator struct { + err error +} + +func (e *erroringEntryCoordinator) BeginAttempt(_ attempt.AttemptContext) (roast.AttemptHandle, error) { + return roast.AttemptHandle{}, e.err +} +func (e *erroringEntryCoordinator) State(_ roast.AttemptHandle) (roast.AttemptState, error) { + return roast.AttemptStatePending, nil +} +func (e *erroringEntryCoordinator) SelectedCoordinator(_ roast.AttemptHandle) (group.MemberIndex, error) { + return 0, nil +} +func (e *erroringEntryCoordinator) RecordEvidence(_ roast.AttemptHandle, _ *roast.LocalEvidenceSnapshot) error { + return nil +} +func (e *erroringEntryCoordinator) AggregateBundle(_ roast.AttemptHandle) (*roast.TransitionMessage, error) { + return nil, nil +} +func (e *erroringEntryCoordinator) VerifyBundle(_ roast.AttemptHandle, _ *roast.TransitionMessage) error { + return nil +} +func (e *erroringEntryCoordinator) NextAttempt( + _ roast.AttemptHandle, _ *roast.TransitionMessage, _ uint, _ []byte, +) (attempt.AttemptContext, error) { + return attempt.AttemptContext{}, nil +} + +func contains(s, substr string) bool { + for i := 0; i+len(substr) <= len(s); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/pkg/frost/signing/roast_retry_executor_entry_test.go b/pkg/frost/signing/roast_retry_executor_entry_test.go new file mode 100644 index 0000000000..478042619d --- /dev/null +++ b/pkg/frost/signing/roast_retry_executor_entry_test.go @@ -0,0 +1,27 @@ +package signing + +import ( + "testing" + + "github.com/ipfs/go-log/v2" +) + +func TestAttemptRoastRetryOrchestrationFromRequest_DefaultBuildIsNoOp(t *testing.T) { + // In the default build, the helper is a permanent stub returning + // (nil, nil) so the executor adapter behaves exactly as in + // Phase 5: no orchestration, no error, no cleanup deferred. + // + // The tagged-build test surface + // (roast_retry_executor_entry_frost_native_test.go) exercises + // the real branching. + cleanup, err := attemptRoastRetryOrchestrationFromRequest( + &NativeExecutionFFISigningRequest{SessionID: "x"}, + log.Logger("test"), + ) + if err != nil { + t.Fatalf("default-build helper must not return an error; got %v", err) + } + if cleanup != nil { + t.Fatal("default-build helper must not return a cleanup function") + } +} diff --git a/pkg/frost/signing/roast_retry_metrics.go b/pkg/frost/signing/roast_retry_metrics.go new file mode 100644 index 0000000000..d1312ab51e --- /dev/null +++ b/pkg/frost/signing/roast_retry_metrics.go @@ -0,0 +1,121 @@ +package signing + +import ( + "sync/atomic" + + "github.com/keep-network/keep-core/pkg/clientinfo" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// roastRetryEvidenceCounters holds cumulative event counts across +// the entire process lifetime. They are bumped whenever a +// metrics-emitting recorder records an event. Exposed to keep- +// core's clientinfo registry via RegisterRoastRetryMetrics, which +// operators invoke at process startup. +// +// The counters are intentionally process-wide rather than per- +// session: operators want to see "how many overflow events did +// the node observe today?" rather than "what was the count for +// the third attempt of session 0x1234?". Per-attempt detail is +// already visible in the TransitionMessage payload. +var ( + roastRetryOverflowEvents atomic.Uint64 + roastRetryRejectEvents atomic.Uint64 + roastRetryConflictEvents atomic.Uint64 +) + +// Application label prefix used by RegisterRoastRetryMetrics when +// registering with clientinfo.Registry.ObserveApplicationSource. +// The registry concatenates this with each per-source name, so the +// final metric labels look like "frost_roast_retry_overflow_events_total". +const roastRetryMetricsApplication = "frost_roast_retry" + +const ( + overflowEventsMetricName = "overflow_events_total" + rejectEventsMetricName = "reject_events_total" + conflictEventsMetricName = "conflict_events_total" +) + +// RegisterRoastRetryMetrics registers the cumulative ROAST-retry +// evidence counters with the supplied clientinfo registry. +// Operators call this from the node's startup sequence so the +// counters appear in the Prometheus scrape alongside the other +// keep-core metrics. +// +// The metrics are emitted in every build but only increment when +// the receive loops actually call into the metrics-emitting +// recorder, which happens only when the ROAST-retry registry is +// populated (i.e. the operator has opted in). In default builds +// the counters stay at zero. +func RegisterRoastRetryMetrics(registry *clientinfo.Registry) { + if registry == nil { + return + } + registry.ObserveApplicationSource( + roastRetryMetricsApplication, + map[string]clientinfo.Source{ + overflowEventsMetricName: func() float64 { + return float64(roastRetryOverflowEvents.Load()) + }, + rejectEventsMetricName: func() float64 { + return float64(roastRetryRejectEvents.Load()) + }, + conflictEventsMetricName: func() float64 { + return float64(roastRetryConflictEvents.Load()) + }, + }, + ) +} + +// metricsEmittingRecorder wraps an attempt.EvidenceRecorder with +// the process-wide cumulative counters declared above. Each +// Record*-class method bumps the matching counter and then +// delegates to the inner recorder so the per-attempt bounded +// snapshot still reflects the event for the NextAttempt policy. +// +// Use newMetricsEmittingRecorder to construct; do not instantiate +// directly. +type metricsEmittingRecorder struct { + inner attempt.EvidenceRecorder +} + +func newMetricsEmittingRecorder( + inner attempt.EvidenceRecorder, +) attempt.EvidenceRecorder { + if inner == nil { + return attempt.NoOpRecorder() + } + return &metricsEmittingRecorder{inner: inner} +} + +func (m *metricsEmittingRecorder) RecordOverflow(sender group.MemberIndex) { + roastRetryOverflowEvents.Add(1) + m.inner.RecordOverflow(sender) +} + +func (m *metricsEmittingRecorder) RecordReject( + sender group.MemberIndex, + reason string, +) { + roastRetryRejectEvents.Add(1) + m.inner.RecordReject(sender, reason) +} + +func (m *metricsEmittingRecorder) RecordConflict(sender group.MemberIndex) { + roastRetryConflictEvents.Add(1) + m.inner.RecordConflict(sender) +} + +func (m *metricsEmittingRecorder) Snapshot() attempt.Evidence { + return m.inner.Snapshot() +} + +// resetRoastRetryMetricsForTest clears the cumulative counters. +// Exposed only for the package's own tests; not a production +// helper. +func resetRoastRetryMetricsForTest() { + roastRetryOverflowEvents.Store(0) + roastRetryRejectEvents.Store(0) + roastRetryConflictEvents.Store(0) +} diff --git a/pkg/frost/signing/roast_retry_metrics_test.go b/pkg/frost/signing/roast_retry_metrics_test.go new file mode 100644 index 0000000000..fd5e015255 --- /dev/null +++ b/pkg/frost/signing/roast_retry_metrics_test.go @@ -0,0 +1,116 @@ +package signing + +import ( + "sync" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" +) + +func TestMetricsEmittingRecorder_IncrementsOnEachCategory(t *testing.T) { + resetRoastRetryMetricsForTest() + t.Cleanup(resetRoastRetryMetricsForTest) + + rec := newMetricsEmittingRecorder(attempt.NewBoundedRecorder()) + rec.RecordOverflow(1) + rec.RecordOverflow(2) + rec.RecordReject(3, "validation_gate_rejected") + rec.RecordConflict(4) + rec.RecordConflict(5) + rec.RecordConflict(6) + + if got := roastRetryOverflowEvents.Load(); got != 2 { + t.Fatalf("overflow counter: got %d want 2", got) + } + if got := roastRetryRejectEvents.Load(); got != 1 { + t.Fatalf("reject counter: got %d want 1", got) + } + if got := roastRetryConflictEvents.Load(); got != 3 { + t.Fatalf("conflict counter: got %d want 3", got) + } +} + +func TestMetricsEmittingRecorder_DelegatesSnapshotToInner(t *testing.T) { + resetRoastRetryMetricsForTest() + t.Cleanup(resetRoastRetryMetricsForTest) + + rec := newMetricsEmittingRecorder(attempt.NewBoundedRecorder()) + rec.RecordOverflow(7) + rec.RecordOverflow(7) + + snap := rec.Snapshot() + if snap.Overflows[7] != 2 { + t.Fatalf( + "inner snapshot must reflect events; got %d want 2", + snap.Overflows[7], + ) + } +} + +func TestMetricsEmittingRecorder_NilInnerFallsBackToNoOp(t *testing.T) { + resetRoastRetryMetricsForTest() + t.Cleanup(resetRoastRetryMetricsForTest) + + rec := newMetricsEmittingRecorder(nil) + // Defensive guard: a nil inner recorder must produce a recorder + // that does not panic on Record* calls. The wrapper substitutes + // a NoOp inner. + rec.RecordOverflow(1) + rec.RecordReject(1, "x") + rec.RecordConflict(1) + // Counters STILL increment with the recommended call sites... + // wait, that's wrong. If inner is nil and we substitute NoOp, + // the wrapper is the NoOp recorder, no counters bumped. + if roastRetryOverflowEvents.Load() != 0 { + t.Fatal("nil inner -> NoOp; counters should stay at zero") + } +} + +func TestRoastRetryRecorderForCollect_WrapsBoundedWithMetricsWhenRegistered(t *testing.T) { + resetRoastRetryMetricsForTest() + ResetRoastRetryRegistrationForTest() + t.Cleanup(resetRoastRetryMetricsForTest) + t.Cleanup(ResetRoastRetryRegistrationForTest) + + // Without registration, the recorder is NoOp -- recording does + // not bump the cumulative counters. + rec := roastRetryRecorderForCollect() + rec.RecordOverflow(1) + if roastRetryOverflowEvents.Load() != 0 { + t.Fatal("no registration -> NoOp recorder -> no counter bump") + } +} + +func TestMetricsEmittingRecorder_ConcurrentCountersAreRaceSafe(t *testing.T) { + resetRoastRetryMetricsForTest() + t.Cleanup(resetRoastRetryMetricsForTest) + + rec := newMetricsEmittingRecorder(attempt.NewBoundedRecorder()) + const workers = 16 + const callsPerWorker = 100 + + var wg sync.WaitGroup + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < callsPerWorker; j++ { + rec.RecordOverflow(1) + } + }() + } + wg.Wait() + + if got := roastRetryOverflowEvents.Load(); got != uint64(workers*callsPerWorker) { + t.Fatalf( + "concurrent counter: got %d want %d", + got, workers*callsPerWorker, + ) + } +} + +func TestRegisterRoastRetryMetrics_NilRegistryIsNoOp(t *testing.T) { + // Defensive: RegisterRoastRetryMetrics(nil) must not panic so + // optional integration paths can pass through nil. + RegisterRoastRetryMetrics(nil) +} diff --git a/pkg/frost/signing/roast_retry_orchestration.go b/pkg/frost/signing/roast_retry_orchestration.go new file mode 100644 index 0000000000..7685df1534 --- /dev/null +++ b/pkg/frost/signing/roast_retry_orchestration.go @@ -0,0 +1,200 @@ +package signing + +// Static-vs-runtime error taxonomy (RFC-21 Phase 6 — Resolved Decision). +// +// The orchestration layer in this file participates in a load-bearing +// decision that prevents split-brain group fracture in the ROAST retry +// path. Errors returned through the orchestration boundary are +// classified into one of two categories, and the consumer (the +// signing-loop dispatcher) routes them accordingly: +// +// STATIC errors -> safe to fall back to the legacy retry path. +// Every honest signer observes the same node-local +// configuration state (registry population, build +// tags) at the same startup, so a fallback decision +// is deterministic across the group. No participant +// fork can arise from a static-error fallback. +// Sentinel: ErrNoRoastRetryCoordinatorRegistered. +// Detected via errors.Is in +// signing_loop_roast_dispatcher.go. +// +// RUNTIME errors -> HARD FAIL. No fallback. Any error that arises +// from per-attempt protocol state (BeginAttempt +// internals, AttemptContext binding mismatches, +// transition-bundle verification failures, etc.) +// can be observed by some participants and not +// others within the same attempt. Falling back to +// legacy under those conditions would leave some +// operators running the new code path and others +// running legacy on the same attempt -- the canonical +// definition of split-brain fracture. The +// orchestration layer therefore returns these as +// bare (non-sentinel) errors that the dispatcher +// treats as terminal. +// +// The classification is enforced at this file's boundary: any error +// surfaced from this package that is intended to permit fallback MUST +// be the ErrNoRoastRetryCoordinatorRegistered sentinel (or wrap it for +// errors.Is matching). Wrapping ANY runtime error in the sentinel is a +// safety regression that re-enables split-brain risk; PR reviewers +// should reject it. +// +// Background: this decision was redirected during Phase 5/6 review. +// The earlier design had Coordinator.BeginAttempt failures fall back to +// the legacy retry path on the assumption that BeginAttempt was a +// cheap idempotent setup. Review identified that BeginAttempt mutates +// per-attempt state (session bindings, evidence recorder) and can fail +// from races with concurrent receives or from peer-supplied protocol +// messages -- both of which produce non-deterministic per-participant +// outcomes. The taxonomy was tightened so only true configuration +// errors are fallback-eligible. + +import ( + "errors" + "fmt" + + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// ErrNoRoastRetryCoordinatorRegistered is returned by +// BeginOrchestrationForSession when the package-level ROAST-retry +// registry has not been populated by a caller. The error is the +// "static configuration" class per the RFC-21 Phase-6 Resolved +// Decision on orchestration error taxonomy: it is safe to fall +// back to the legacy retry path because every honest signer +// observes the same registry state at the same node startup, so +// the fallback decision is deterministic across the group. +// +// Use errors.Is to detect. +var ErrNoRoastRetryCoordinatorRegistered = errors.New( + "roast orchestration: no coordinator registered", +) + +// BeginOrchestrationForSession encapsulates the per-session +// BeginAttempt + binding-population step the RFC-21 Phase 5 +// orchestration layer performs. Callers in the layer above the +// FROST signing primitive invoke it at session start; the returned +// cleanup function is the matching unbinding step the caller +// defers. +// +// Phase 5.2 ships the helper; Phase 6 wires production call sites +// to invoke it (and to feed the AttemptContext from the resolver +// adapter, etc.). +// +// When the ROAST-retry registry is empty (default build, no caller +// has registered a coordinator), the helper returns an error so +// the caller can fall back to legacy behaviour. The two-arg +// "shape" -- (handle, cleanup, error) -- forces the caller to +// handle the absence of a coordinator explicitly rather than +// silently dropping the orchestration. +func BeginOrchestrationForSession( + sessionID string, + ctx attempt.AttemptContext, +) (roast.AttemptHandle, func(), error) { + if err := EnsureRoastRetryReadinessOptIn(); err != nil { + return roast.AttemptHandle{}, nil, fmt.Errorf( + "roast orchestration: %w", + err, + ) + } + deps, ok := RegisteredRoastRetryCoordinator() + if !ok { + return roast.AttemptHandle{}, nil, fmt.Errorf( + "%w: caller should fall back to legacy behaviour", + ErrNoRoastRetryCoordinatorRegistered, + ) + } + if deps.Coordinator == nil { + return roast.AttemptHandle{}, nil, fmt.Errorf( + "roast orchestration: registered RoastRetryDeps has nil Coordinator", + ) + } + handle, err := deps.Coordinator.BeginAttempt(ctx) + if err != nil { + return roast.AttemptHandle{}, nil, fmt.Errorf( + "roast orchestration: begin attempt for session %q: %w", + sessionID, + err, + ) + } + SetCurrentAttemptHandleForSession(sessionID, handle, ctx) + cleanup := func() { + // RFC-21 Phase 7.1: if this node is the elected + // coordinator and the attempt is still in the Collecting + // state at cleanup time (i.e. it did not succeed via + // signature aggregation), produce the TransitionMessage + // and stash it in the per-session bundle registry. Phase + // 7.2's ROAST signingParticipantSelector consumes the + // stashed bundle to compute the next attempt's + // IncludedSet via EvaluateRoastRetryForSigning. + // + // Failures are best-effort and silent: a panic in the + // deferred cleanup is materially worse than a missing + // transition bundle (the next attempt's selector falls + // back to the legacy retry shuffle), so we swallow errors + // rather than propagate them. + maybeProduceTransitionBundle(sessionID, handle, deps) + ClearCurrentAttemptHandleForSession(sessionID) + } + return handle, cleanup, nil +} + +// maybeProduceTransitionBundle attempts to call AggregateBundle on +// the registered Coordinator when (a) the local node is the +// elected coordinator for the attempt and (b) the attempt has not +// already transitioned. The result is stashed via +// RecordTransitionBundleForSession (a no-op in default build); on +// any error path the function returns silently because cleanup +// must not break the signing-flow contract. +// +// In the default build this still compiles because +// RecordTransitionBundleForSession is a no-op stub; calls to +// roast.Coordinator methods compile because pkg/frost/roast is +// not build-tagged. +func maybeProduceTransitionBundle( + sessionID string, + handle roast.AttemptHandle, + deps RoastRetryDeps, +) { + if deps.Coordinator == nil { + return + } + if deps.SelfMember == 0 { + // Without a known self-member, we cannot determine + // whether to aggregate. Skip. + return + } + elected, err := deps.Coordinator.SelectedCoordinator(handle) + if err != nil { + return + } + if elected != group.MemberIndex(deps.SelfMember) { + return + } + state, err := deps.Coordinator.State(handle) + if err != nil { + return + } + if state != roast.AttemptStateCollecting { + // Already transitioned or succeeded -- nothing to do. + return + } + bundle, err := deps.Coordinator.AggregateBundle(handle) + if err != nil { + // Best-effort; the next attempt's selector will fall + // back to the legacy retry shuffle. + return + } + RecordTransitionBundleForSession(sessionID, bundle) +} + +// EndOrchestrationForSession is a convenience for callers that +// did not capture the cleanup function from +// BeginOrchestrationForSession (e.g. callers that pass session +// ownership across function boundaries). It is equivalent to +// invoking the cleanup function returned by Begin. +func EndOrchestrationForSession(sessionID string) { + ClearCurrentAttemptHandleForSession(sessionID) +} diff --git a/pkg/frost/signing/roast_retry_orchestration_bundle_test.go b/pkg/frost/signing/roast_retry_orchestration_bundle_test.go new file mode 100644 index 0000000000..38ca6acde9 --- /dev/null +++ b/pkg/frost/signing/roast_retry_orchestration_bundle_test.go @@ -0,0 +1,215 @@ +//go:build frost_roast_retry + +package signing + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// signingForBundleContext constructs an attempt context whose +// SelectCoordinator will deterministically pick member 1 (for the +// sake of this test). Real production deployments use the +// rotating selection; here we pin a stable handle for assertion. +func signingForBundleContext(t *testing.T, members []group.MemberIndex) attempt.AttemptContext { + t.Helper() + ctx, err := attempt.NewAttemptContext( + "orchestration-bundle-test", + "key-group", + []byte{0x01, 0x02, 0x03}, + [attempt.MessageDigestLength]byte{0xab}, + 0, + members, + nil, + ) + if err != nil { + t.Fatalf("ctx: %v", err) + } + return ctx +} + +// realCoordinatorForBundleTest returns an in-memory coordinator +// with NoOp signer/verifier so AggregateBundle path runs end-to- +// end without crypto setup. The coordinator's selfMember is the +// elected coordinator computed from the test context, so +// maybeProduceTransitionBundle invokes AggregateBundle. +func realCoordinatorForBundleTest( + t *testing.T, + ctx attempt.AttemptContext, +) (roast.Coordinator, group.MemberIndex) { + t.Helper() + scratch := roast.NewInMemoryCoordinator() + hScratch, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(hScratch) + coord := roast.NewInMemoryCoordinatorWithSigning( + elected, + roast.NoOpSigner(), + roast.NoOpSignatureVerifier(), + ) + return coord, elected +} + +func TestCleanup_ProducesBundleWhenElectedCoordinator(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + ResetTransitionBundleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + t.Cleanup(ResetTransitionBundleRegistryForTest) + + ctx := signingForBundleContext(t, []group.MemberIndex{1, 2, 3, 4, 5}) + coord, elected := realCoordinatorForBundleTest(t, ctx) + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: coord, + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: uint32(elected), + }) + + const sessionID = "bundle-producer-session" + handle, cleanup, err := BeginOrchestrationForSession(sessionID, ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + + // Seed at least one snapshot so AggregateBundle's + // non-empty-bundle validation passes. + snap := roast.NewLocalEvidenceSnapshot(elected, ctx.Hash(), attempt.Evidence{}) + // NoOpSigner returns empty bytes but the signature-verification + // pre-check rejects zero-length signatures. Provide a dummy + // non-empty signature; the NoOp verifier accepts any byte + // sequence. + snap.OperatorSignature = []byte{0x01} + if err := coord.RecordEvidence(handle, snap); err != nil { + t.Fatalf("record evidence: %v", err) + } + + // Cleanup must produce + record a bundle (we're the elected + // coordinator and the attempt is still Collecting). + cleanup() + + bundle, ok := TransitionBundleForSession(sessionID) + if !ok { + t.Fatal("elected coordinator's cleanup must produce a bundle") + } + if bundle == nil { + t.Fatal("recorded bundle must not be nil") + } + if bundle.CoordinatorID() != elected { + t.Fatalf( + "bundle coordinator id %d != elected %d", + bundle.CoordinatorID(), elected, + ) + } +} + +func TestCleanup_DoesNotProduceBundleWhenNotElectedCoordinator(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + ResetTransitionBundleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + t.Cleanup(ResetTransitionBundleRegistryForTest) + + ctx := signingForBundleContext(t, []group.MemberIndex{1, 2, 3, 4, 5}) + _, elected := realCoordinatorForBundleTest(t, ctx) + + // Register with a SELF that is NOT the elected coordinator. + nonElected := group.MemberIndex(elected + 10) // arbitrary non-elected + for _, m := range ctx.IncludedSet { + if m != elected { + nonElected = m + break + } + } + + // Use a fresh coordinator bound to the non-elected member. + coord := roast.NewInMemoryCoordinatorWithSigning( + nonElected, + roast.NoOpSigner(), + roast.NoOpSignatureVerifier(), + ) + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: coord, + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: uint32(nonElected), + }) + + const sessionID = "non-elected-session" + _, cleanup, err := BeginOrchestrationForSession(sessionID, ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + cleanup() + + if _, ok := TransitionBundleForSession(sessionID); ok { + t.Fatal("non-elected coordinator must not produce a bundle") + } +} + +func TestCleanup_AggregateBundleErrorIsSwallowed(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + ResetTransitionBundleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + t.Cleanup(ResetTransitionBundleRegistryForTest) + + // Use the standard coordinator. AggregateBundle will fail + // because the elected coordinator was 'self' but we never + // recorded any snapshots in the coordinator (so the bundle + // would be empty). Actually -- empty bundle violates + // validation. Let me set up a scenario where Aggregate fails. + // + // Strategy: register a coordinator whose BeginAttempt succeeds + // but AggregateBundle returns ErrAttemptStateInvalid because + // we manually transition the state through State. Simpler: + // just call cleanup() twice. The second call sees the + // already-transitioned state and bails out cleanly without + // recording a duplicate bundle. + + ctx := signingForBundleContext(t, []group.MemberIndex{1, 2, 3, 4, 5}) + coord, elected := realCoordinatorForBundleTest(t, ctx) + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: coord, + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: uint32(elected), + }) + + const sessionID = "double-cleanup-session" + handle, cleanup, err := BeginOrchestrationForSession(sessionID, ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + + // Seed snapshot so the first cleanup's AggregateBundle + // succeeds. + snap := roast.NewLocalEvidenceSnapshot(elected, ctx.Hash(), attempt.Evidence{}) + // NoOpSigner returns empty bytes but the signature-verification + // pre-check rejects zero-length signatures. Provide a dummy + // non-empty signature; the NoOp verifier accepts any byte + // sequence. + snap.OperatorSignature = []byte{0x01} + if err := coord.RecordEvidence(handle, snap); err != nil { + t.Fatalf("record evidence: %v", err) + } + + // First cleanup -- bundle recorded. + cleanup() + if _, ok := TransitionBundleForSession(sessionID); !ok { + t.Fatal("first cleanup must record bundle") + } + + // Second cleanup -- state is now Transitioned. AggregateBundle + // returns ErrAttemptStateInvalid; the helper must swallow the + // error rather than panic. + cleanup() // Must not panic. +} diff --git a/pkg/frost/signing/roast_retry_orchestration_frost_roast_retry_test.go b/pkg/frost/signing/roast_retry_orchestration_frost_roast_retry_test.go new file mode 100644 index 0000000000..6ef63d85ab --- /dev/null +++ b/pkg/frost/signing/roast_retry_orchestration_frost_roast_retry_test.go @@ -0,0 +1,305 @@ +//go:build frost_roast_retry + +package signing + +import ( + "errors" + "strings" + "testing" + "time" + + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func newOrchestrationTestContext(t *testing.T) attempt.AttemptContext { + t.Helper() + ctx, err := attempt.NewAttemptContext( + "orchestration-session", + "key-group-orchestration", + []byte{0x01, 0x02}, + [attempt.MessageDigestLength]byte{0x77}, + 0, + []group.MemberIndex{1, 2, 3, 4, 5}, + nil, + ) + if err != nil { + t.Fatalf("ctx: %v", err) + } + return ctx +} + +func TestBeginOrchestrationForSession_HappyPath(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + ctx := newOrchestrationTestContext(t) + handle, cleanup, err := BeginOrchestrationForSession("session-A", ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + if cleanup == nil { + t.Fatal("cleanup must not be nil") + } + + // Binding must exist. + gotHandle, gotCtx, ok := currentAttemptHandleForCollect("session-A") + if !ok { + t.Fatal("binding must exist after Begin") + } + if gotHandle != handle { + t.Fatal("binding handle mismatch") + } + if gotCtx.Hash() != ctx.Hash() { + t.Fatal("binding context mismatch") + } + + cleanup() + if _, _, ok := currentAttemptHandleForCollect("session-A"); ok { + t.Fatal("binding must be cleared after cleanup") + } +} + +func TestBeginOrchestrationForSession_ErrorsWhenRegistryEmpty(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + // Readiness env var is set; the registry is empty -- we expect + // the registry-empty error, not the env-var error. + _, _, err := BeginOrchestrationForSession("session-X", newOrchestrationTestContext(t)) + if err == nil { + t.Fatal("expected error when registry is empty") + } + if !strings.Contains(err.Error(), "no coordinator registered") { + t.Fatalf("error must mention missing registration; got %v", err) + } +} + +func TestBeginOrchestrationForSession_ErrorsWhenReadinessOptInUnset(t *testing.T) { + // Explicitly unset, in case the test runner inherits the env var + // from outside. + t.Setenv(RoastRetryReadinessOptInEnvVar, "") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + // Even with a registered coordinator, the readiness env var + // short-circuits orchestration. This is the load-bearing safety + // property: production builds with the frost_roast_retry tag + // still cannot enter the orchestration path without an explicit + // operator decision. + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + _, _, err := BeginOrchestrationForSession("session-no-optin", newOrchestrationTestContext(t)) + if !errors.Is(err, ErrRoastRetryReadinessOptOut) { + t.Fatalf("expected ErrRoastRetryReadinessOptOut, got %v", err) + } +} + +func TestBeginOrchestrationForSession_ErrorsWhenCoordinatorNil(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: nil, + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + _, _, err := BeginOrchestrationForSession("session-Y", newOrchestrationTestContext(t)) + if err == nil { + t.Fatal("expected error when Coordinator is nil") + } + if !strings.Contains(err.Error(), "nil Coordinator") { + t.Fatalf("error must mention nil coordinator; got %v", err) + } +} + +func TestBeginOrchestrationForSession_PropagatesBeginAttemptError(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + // A coordinator whose BeginAttempt always fails. + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: &erroringCoordinator{err: errors.New("synthetic begin failure")}, + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + _, _, err := BeginOrchestrationForSession("session-Z", newOrchestrationTestContext(t)) + if err == nil { + t.Fatal("expected error from coordinator") + } + if !strings.Contains(err.Error(), "synthetic begin failure") { + t.Fatalf("error must wrap underlying cause; got %v", err) + } +} + +func TestEndOrchestrationForSession_RemovesBinding(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newOrchestrationTestContext(t) + SetCurrentAttemptHandleForSession("session-end", roast.AttemptHandle{}, ctx) + + if _, _, ok := currentAttemptHandleForCollect("session-end"); !ok { + t.Fatal("setup: binding must exist") + } + EndOrchestrationForSession("session-end") + if _, _, ok := currentAttemptHandleForCollect("session-end"); ok { + t.Fatal("binding must be removed after End") + } +} + +func TestEvictStaleSessionHandleBindings_RemovesOldEntries(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + // Two bindings with different ages. + ctx := newOrchestrationTestContext(t) + SetCurrentAttemptHandleForSession("session-old", roast.AttemptHandle{}, ctx) + // Backdate by forcing the timestamp. + sessionAttemptBindingMu.Lock() + b := sessionAttemptBindings["session-old"] + b.createdAt = time.Now().Add(-10 * time.Minute) + sessionAttemptBindings["session-old"] = b + sessionAttemptBindingMu.Unlock() + + SetCurrentAttemptHandleForSession("session-new", roast.AttemptHandle{}, ctx) + + // Sweep with 5-minute TTL: old must be evicted, new must survive. + evicted := evictStaleSessionHandleBindings(5 * time.Minute) + if evicted != 1 { + t.Fatalf("expected 1 eviction, got %d", evicted) + } + if _, _, ok := currentAttemptHandleForCollect("session-old"); ok { + t.Fatal("session-old must be evicted") + } + if _, _, ok := currentAttemptHandleForCollect("session-new"); !ok { + t.Fatal("session-new must survive") + } +} + +func TestEvictStaleSessionHandleBindings_LeavesFreshEntries(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newOrchestrationTestContext(t) + SetCurrentAttemptHandleForSession("session-fresh", roast.AttemptHandle{}, ctx) + + // Sweep with the default 2-hour TTL: nothing should be evicted. + evicted := evictStaleSessionHandleBindings(SessionHandleBindingTTL) + if evicted != 0 { + t.Fatalf("expected 0 evictions for fresh binding, got %d", evicted) + } +} + +func TestSessionHandleBindingTTL_MatchesRFC(t *testing.T) { + if SessionHandleBindingTTL != 2*time.Hour { + t.Fatalf( + "RFC-21 specifies a 2-hour default TTL; constant is %s", + SessionHandleBindingTTL, + ) + } +} + +func TestStartSessionHandleSweeper_IsIdempotent(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + StartSessionHandleSweeper() + StartSessionHandleSweeper() + StartSessionHandleSweeper() + // sync.Once means only one goroutine started; we don't have a + // direct observable, but ResetSessionHandleRegistryForTest will + // close the stop channel and the goroutine will exit cleanly. + // If sync.Once were broken, double-close on the stop channel + // would panic during cleanup. +} + +func TestRegisterRoastRetryCoordinator_StartsSweeper(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + // Register again to verify sync.Once prevents a second + // sweeper. + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 2, + }) + + // Reset should not panic (would panic on double-close if + // sync.Once failed). + ResetSessionHandleRegistryForTest() +} + +// erroringCoordinator returns a synthetic error from BeginAttempt. +// Other methods return zero values or nil; tests that need them +// should use a real coordinator. +type erroringCoordinator struct { + err error +} + +func (e *erroringCoordinator) BeginAttempt(_ attempt.AttemptContext) (roast.AttemptHandle, error) { + return roast.AttemptHandle{}, e.err +} +func (e *erroringCoordinator) State(_ roast.AttemptHandle) (roast.AttemptState, error) { + return roast.AttemptStatePending, nil +} +func (e *erroringCoordinator) SelectedCoordinator(_ roast.AttemptHandle) (group.MemberIndex, error) { + return 0, nil +} +func (e *erroringCoordinator) RecordEvidence(_ roast.AttemptHandle, _ *roast.LocalEvidenceSnapshot) error { + return nil +} +func (e *erroringCoordinator) AggregateBundle(_ roast.AttemptHandle) (*roast.TransitionMessage, error) { + return nil, nil +} +func (e *erroringCoordinator) VerifyBundle(_ roast.AttemptHandle, _ *roast.TransitionMessage) error { + return nil +} +func (e *erroringCoordinator) NextAttempt( + _ roast.AttemptHandle, _ *roast.TransitionMessage, _ uint, _ []byte, +) (attempt.AttemptContext, error) { + return attempt.AttemptContext{}, nil +} diff --git a/pkg/frost/signing/roast_retry_orchestration_test.go b/pkg/frost/signing/roast_retry_orchestration_test.go new file mode 100644 index 0000000000..08e42777cc --- /dev/null +++ b/pkg/frost/signing/roast_retry_orchestration_test.go @@ -0,0 +1,37 @@ +package signing + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestBeginOrchestrationForSession_DefaultBuildReturnsError(t *testing.T) { + // In the default build, RegisteredRoastRetryCoordinator always + // returns (zero, false), so the orchestration helper must + // return an error directing the caller to fall back to legacy + // behaviour. This guarantees no production caller can + // accidentally "succeed" into orchestration when the build tag + // is off. + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + + ctx, err := attempt.NewAttemptContext( + "session-default-build", + "key-group", + []byte{0x01}, + [attempt.MessageDigestLength]byte{0x77}, + 0, + []group.MemberIndex{1, 2, 3}, + nil, + ) + if err != nil { + t.Fatalf("ctx: %v", err) + } + + _, _, err = BeginOrchestrationForSession("session-default-build", ctx) + if err == nil { + t.Fatal("default build must return error from BeginOrchestrationForSession") + } +} diff --git a/pkg/frost/signing/roast_retry_readiness.go b/pkg/frost/signing/roast_retry_readiness.go new file mode 100644 index 0000000000..1bd700230c --- /dev/null +++ b/pkg/frost/signing/roast_retry_readiness.go @@ -0,0 +1,60 @@ +package signing + +import ( + "errors" + "fmt" + "os" + "strings" +) + +// RoastRetryReadinessOptInEnvVar is the environment variable name +// operators must set to "true" to opt in to RFC-21 ROAST retry +// activation. The variable is read per call -- not cached -- so an +// operator can flip it during a debugging session without +// restarting the node. +// +// Pattern matches the existing +// KEEP_CORE_FROST_TBTC_SIGNER_ACCEPT_SCAFFOLD_KEY_GROUP env var +// from PR #3960: a build tag enables the code path, an env var +// enables the wiring, both must agree for the feature to be live. +const RoastRetryReadinessOptInEnvVar = "KEEP_CORE_FROST_ROAST_RETRY_ENABLED" + +// ErrRoastRetryReadinessOptOut is the error +// EnsureRoastRetryReadinessOptIn returns when the env var is unset +// or set to anything other than "true". Use errors.Is to detect. +var ErrRoastRetryReadinessOptOut = errors.New( + "roast retry readiness: operator opt-in env var is not set to true", +) + +// EnsureRoastRetryReadinessOptIn reads the +// RoastRetryReadinessOptInEnvVar environment variable and returns +// nil if it is set to the string "true" (case-insensitive, +// whitespace-trimmed). Returns ErrRoastRetryReadinessOptOut +// otherwise. +// +// Callers in the orchestration layer invoke this before +// RegisterRoastRetryCoordinator so production builds with the +// frost_roast_retry build tag still refuse to wire orchestration +// without an explicit operator decision. +// +// The function is per-call (not cached) so operators can flip the +// env var dynamically during debugging. +func EnsureRoastRetryReadinessOptIn() error { + if !RoastRetryReadinessOptInEnabled() { + return fmt.Errorf( + "%w: set %s=true to enable", + ErrRoastRetryReadinessOptOut, + RoastRetryReadinessOptInEnvVar, + ) + } + return nil +} + +// RoastRetryReadinessOptInEnabled reports whether the readiness +// env var is currently set to "true". Cheap to call; use this when +// you need a boolean (e.g., to gate a log message) and +// EnsureRoastRetryReadinessOptIn when you need an error. +func RoastRetryReadinessOptInEnabled() bool { + value := strings.TrimSpace(os.Getenv(RoastRetryReadinessOptInEnvVar)) + return strings.EqualFold(value, "true") +} diff --git a/pkg/frost/signing/roast_retry_readiness_test.go b/pkg/frost/signing/roast_retry_readiness_test.go new file mode 100644 index 0000000000..9eb0e82746 --- /dev/null +++ b/pkg/frost/signing/roast_retry_readiness_test.go @@ -0,0 +1,82 @@ +package signing + +import ( + "errors" + "strings" + "testing" +) + +func TestEnsureRoastRetryReadinessOptIn_AcceptsTrue(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + if err := EnsureRoastRetryReadinessOptIn(); err != nil { + t.Fatalf("expected nil error, got %v", err) + } +} + +func TestEnsureRoastRetryReadinessOptIn_AcceptsTrueCaseInsensitive(t *testing.T) { + cases := []string{"true", "True", "TRUE", "tRuE"} + for _, value := range cases { + t.Run(value, func(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, value) + if err := EnsureRoastRetryReadinessOptIn(); err != nil { + t.Fatalf("expected nil error for %q, got %v", value, err) + } + }) + } +} + +func TestEnsureRoastRetryReadinessOptIn_AcceptsTrimmedWhitespace(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, " true ") + if err := EnsureRoastRetryReadinessOptIn(); err != nil { + t.Fatalf("expected nil error for whitespace-padded 'true', got %v", err) + } +} + +func TestEnsureRoastRetryReadinessOptIn_RejectsUnset(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "") + err := EnsureRoastRetryReadinessOptIn() + if !errors.Is(err, ErrRoastRetryReadinessOptOut) { + t.Fatalf("expected ErrRoastRetryReadinessOptOut, got %v", err) + } + if !strings.Contains(err.Error(), RoastRetryReadinessOptInEnvVar) { + t.Fatalf( + "error must mention the env var name to guide operators; got %v", + err, + ) + } +} + +func TestEnsureRoastRetryReadinessOptIn_RejectsOtherValues(t *testing.T) { + cases := []string{"false", "1", "yes", "TRUE_", "tru", "anything"} + for _, value := range cases { + t.Run(value, func(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, value) + err := EnsureRoastRetryReadinessOptIn() + if !errors.Is(err, ErrRoastRetryReadinessOptOut) { + t.Fatalf("expected error for %q, got nil", value) + } + }) + } +} + +func TestRoastRetryReadinessOptInEnabled_MirrorsEnsureResult(t *testing.T) { + t.Setenv(RoastRetryReadinessOptInEnvVar, "true") + if !RoastRetryReadinessOptInEnabled() { + t.Fatal("expected true when env var set to true") + } + t.Setenv(RoastRetryReadinessOptInEnvVar, "false") + if RoastRetryReadinessOptInEnabled() { + t.Fatal("expected false when env var set to false") + } +} + +func TestRoastRetryReadinessOptInEnvVar_MatchesRFC(t *testing.T) { + const expected = "KEEP_CORE_FROST_ROAST_RETRY_ENABLED" + if RoastRetryReadinessOptInEnvVar != expected { + t.Fatalf( + "env var name drifted: got %q want %q (must match RFC-21 Phase 5)", + RoastRetryReadinessOptInEnvVar, + expected, + ) + } +} diff --git a/pkg/frost/signing/roast_retry_recorder.go b/pkg/frost/signing/roast_retry_recorder.go new file mode 100644 index 0000000000..4bc2e292d7 --- /dev/null +++ b/pkg/frost/signing/roast_retry_recorder.go @@ -0,0 +1,38 @@ +package signing + +import ( + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" +) + +// roastRetryRecorderForCollect returns the EvidenceRecorder a FROST +// receive loop should use for its current call. +// +// When the package-level ROAST-retry registry is empty (default +// build, or no caller has invoked RegisterRoastRetryCoordinator), +// the receive loops fall back to attempt.NoOpRecorder() so receive +// semantics match Phase 2 exactly: overflow events are discarded +// without observable effect. +// +// When the registry has a coordinator, the function returns a fresh +// attempt.NewBoundedRecorder(). Each call returns a NEW recorder so +// per-collect evidence does not leak across calls. The caller is +// responsible for capturing the returned recorder if it intends to +// inspect Snapshot() at end-of-collect; in Phase 4.2 we only wire +// the call sites to use the registry. PR 4.3 captures the recorder +// reference and submits its snapshot via Coordinator.RecordEvidence. +// +// This helper is intentionally not build-tagged: it delegates to +// RegisteredRoastRetryCoordinator (which IS build-tagged via the +// roast_retry_registration_* files), so the default-build path +// always sees an empty registry and returns NoOp without paying any +// coordinator-construction cost. +func roastRetryRecorderForCollect() attempt.EvidenceRecorder { + if _, ok := RegisteredRoastRetryCoordinator(); !ok { + return attempt.NoOpRecorder() + } + // Wrap the bounded recorder with the metrics-emitting + // decorator so RecordOverflow/Reject/Conflict bump the + // process-wide cumulative counters that + // RegisterRoastRetryMetrics exposes to clientinfo. + return newMetricsEmittingRecorder(attempt.NewBoundedRecorder()) +} diff --git a/pkg/frost/signing/roast_retry_recorder_frost_roast_retry_test.go b/pkg/frost/signing/roast_retry_recorder_frost_roast_retry_test.go new file mode 100644 index 0000000000..96d5ab6a4e --- /dev/null +++ b/pkg/frost/signing/roast_retry_recorder_frost_roast_retry_test.go @@ -0,0 +1,56 @@ +//go:build frost_roast_retry + +package signing + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestRoastRetryRecorderForCollect_RecordsOverflowWhenRegistered(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + + rec := roastRetryRecorderForCollect() + const sender group.MemberIndex = 3 + rec.RecordOverflow(sender) + rec.RecordOverflow(sender) + snap := rec.Snapshot() + if got := snap.Overflows[sender]; got != 2 { + t.Fatalf( + "expected bounded recorder to accumulate overflows; got %d for sender %d", + got, sender, + ) + } +} + +func TestRoastRetryRecorderForCollect_FallsBackToNoOpAfterReset(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + ResetRoastRetryRegistrationForTest() + + rec := roastRetryRecorderForCollect() + rec.RecordOverflow(5) + if got := rec.Snapshot().Overflows[5]; got != 0 { + t.Fatalf( + "after reset the recorder must be NoOp; got count %d", + got, + ) + } +} diff --git a/pkg/frost/signing/roast_retry_recorder_test.go b/pkg/frost/signing/roast_retry_recorder_test.go new file mode 100644 index 0000000000..cd6fd04089 --- /dev/null +++ b/pkg/frost/signing/roast_retry_recorder_test.go @@ -0,0 +1,76 @@ +package signing + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestRoastRetryRecorderForCollect_NoOpWhenRegistryEmpty(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + rec := roastRetryRecorderForCollect() + // Record an overflow. NoOp recorders must show zero in their + // snapshot regardless of input. + rec.RecordOverflow(group.MemberIndex(1)) + rec.RecordOverflow(group.MemberIndex(2)) + snap := rec.Snapshot() + if len(snap.Overflows) != 0 { + t.Fatalf( + "expected NoOp recorder when registry empty; got %d overflow entries", + len(snap.Overflows), + ) + } +} + +func TestRoastRetryRecorderForCollect_BoundedWhenRegistryPopulated(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + // In the default build, RegisterRoastRetryCoordinator is a + // no-op stub; the registry stays empty and this test asserts + // the same NoOp behaviour as the previous test. The tagged + // build (roast_retry_recorder_frost_roast_retry_test.go) is + // where we assert real BoundedRecorder allocation. + RegisterRoastRetryCoordinator(RoastRetryDeps{SelfMember: 1}) + + rec := roastRetryRecorderForCollect() + if rec == nil { + t.Fatal("recorder must never be nil") + } + // We don't assert the *type* of recorder here because tagged + // vs default builds will return different concrete types; the + // observable contract is that Snapshot() always works. + _ = rec.Snapshot() +} + +func TestRoastRetryRecorderForCollect_NewRecorderEachCall(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + // Even in the default build, the helper returns a recorder + // instance per call. We assert that the snapshot for the first + // call does not leak into the second. + a := roastRetryRecorderForCollect() + a.RecordOverflow(group.MemberIndex(1)) + b := roastRetryRecorderForCollect() + bSnap := b.Snapshot() + if got := bSnap.Overflows[1]; got != 0 { + t.Fatalf( + "second recorder must not share state with first; got overflow count %d for sender 1", + got, + ) + } + // Sanity-check: in the NoOp path, even the first recorder's + // snapshot is empty. + if got := a.Snapshot().Overflows[1]; got != 0 { + // NoOp path: must be 0. + // Tagged path: also 0 (we only registered above; this test + // runs default-build). + _ = got + } + // Silence unused. + _ = attempt.NoOpRecorder() +} diff --git a/pkg/frost/signing/roast_retry_registration_default_build.go b/pkg/frost/signing/roast_retry_registration_default_build.go new file mode 100644 index 0000000000..6a257405b8 --- /dev/null +++ b/pkg/frost/signing/roast_retry_registration_default_build.go @@ -0,0 +1,54 @@ +//go:build !frost_roast_retry + +package signing + +import "github.com/keep-network/keep-core/pkg/frost/roast" + +// RoastRetryDeps bundles the per-process dependencies the FROST +// receive loops need to participate in RFC-21 Phase-4 coordinator- +// driven evidence flow: +// +// - Coordinator drives BeginAttempt / RecordEvidence / AggregateBundle +// / VerifyBundle / NextAttempt. +// - Signer produces operator-key signatures over canonical +// snapshot and bundle bytes. +// - Verifier validates signatures on inbound snapshots and bundles. +// +// The type is exported in every build so callers can construct it +// without conditional compilation. In the default build the registry +// is a permanent no-op stub: the receive loops cannot find a +// registered coordinator and therefore fall back to the Phase-2 +// `attempt.NoOpRecorder()` behaviour, preserving exact pre-RFC-21 +// receive semantics. +// +// The real registry behind the `frost_roast_retry` build tag is in +// roast_retry_registration_frost_roast_retry.go. +type RoastRetryDeps struct { + Coordinator roast.Coordinator + Signer roast.Signer + Verifier roast.SignatureVerifier + // SelfMember is the local node's member index. The Coordinator + // is already bound to this value via NewInMemoryCoordinatorWithSigning, + // but receivers need it independently so they can correlate + // AttemptHandles with their own snapshots in later Phase-4 PRs. + SelfMember uint32 +} + +// RegisterRoastRetryCoordinator is a no-op in the default build. +// Callers in production code may invoke it unconditionally; the +// registration only takes effect when the `frost_roast_retry` build +// tag is active. +func RegisterRoastRetryCoordinator(_ RoastRetryDeps) {} + +// RegisteredRoastRetryCoordinator returns (zero, false) in the +// default build, signalling to receivers that ROAST-retry plumbing +// is not active and they should continue to use the Phase-2 +// NoOpRecorder fallback. +func RegisteredRoastRetryCoordinator() (RoastRetryDeps, bool) { + return RoastRetryDeps{}, false +} + +// ResetRoastRetryRegistrationForTest is a no-op in the default +// build. Exposed so tests can call it unconditionally regardless of +// which build is active. +func ResetRoastRetryRegistrationForTest() {} diff --git a/pkg/frost/signing/roast_retry_registration_default_build_test.go b/pkg/frost/signing/roast_retry_registration_default_build_test.go new file mode 100644 index 0000000000..91b0135ba4 --- /dev/null +++ b/pkg/frost/signing/roast_retry_registration_default_build_test.go @@ -0,0 +1,27 @@ +//go:build !frost_roast_retry + +package signing + +import "testing" + +func TestRoastRetryRegistration_DefaultBuildIsStub(t *testing.T) { + // Register a non-zero dependency set. Because the default build + // is a no-op stub, the registry must remain empty. + deps := RoastRetryDeps{SelfMember: 7} + RegisterRoastRetryCoordinator(deps) + got, ok := RegisteredRoastRetryCoordinator() + if ok { + t.Fatalf("default build must report not-registered; got ok=true, deps=%+v", got) + } + if got != (RoastRetryDeps{}) { + t.Fatalf("default build must return zero value; got %+v", got) + } +} + +func TestRoastRetryRegistration_DefaultBuildResetIsNoOp(t *testing.T) { + // Reset should not panic even though there is no real state. + ResetRoastRetryRegistrationForTest() + if _, ok := RegisteredRoastRetryCoordinator(); ok { + t.Fatal("default build registry should remain empty after reset") + } +} diff --git a/pkg/frost/signing/roast_retry_registration_frost_roast_retry.go b/pkg/frost/signing/roast_retry_registration_frost_roast_retry.go new file mode 100644 index 0000000000..324da6bf22 --- /dev/null +++ b/pkg/frost/signing/roast_retry_registration_frost_roast_retry.go @@ -0,0 +1,71 @@ +//go:build frost_roast_retry + +package signing + +import ( + "sync" + + "github.com/keep-network/keep-core/pkg/frost/roast" +) + +// RoastRetryDeps bundles the per-process dependencies the FROST +// receive loops need under the frost_roast_retry build tag. See the +// default-build file for the doc contract; this declaration is the +// real one used when the build tag is active. +type RoastRetryDeps struct { + Coordinator roast.Coordinator + Signer roast.Signer + Verifier roast.SignatureVerifier + SelfMember uint32 +} + +// roastRetryRegistration is the package-private registry slot. Only +// one set of dependencies can be registered at a time; later +// registrations overwrite earlier ones. Callers wanting to test +// reset behaviour use ResetRoastRetryRegistrationForTest. +var ( + roastRetryRegistrationMu sync.RWMutex + roastRetryRegistration RoastRetryDeps + roastRetryRegistered bool +) + +// RegisterRoastRetryCoordinator stores the per-process ROAST-retry +// dependencies the receive loops will pick up on their next call. +// Safe for concurrent registration / lookup; a later registration +// fully replaces an earlier one (this is the documented behaviour -- +// reconfiguring at runtime is intentional). +// +// As a side effect, the first registration starts the +// session-handle sweeper goroutine that evicts orphaned bindings +// (RFC-21 Phase 5.2 defence-in-depth backstop). Subsequent +// registrations do not restart the sweeper. +func RegisterRoastRetryCoordinator(deps RoastRetryDeps) { + roastRetryRegistrationMu.Lock() + roastRetryRegistration = deps + roastRetryRegistered = true + roastRetryRegistrationMu.Unlock() + StartSessionHandleSweeper() +} + +// RegisteredRoastRetryCoordinator returns the currently-registered +// dependencies and true, or the zero value and false if nothing has +// been registered yet. Receivers use the boolean to decide between +// the bounded recorder path and the Phase-2 NoOp fallback. +func RegisteredRoastRetryCoordinator() (RoastRetryDeps, bool) { + roastRetryRegistrationMu.RLock() + defer roastRetryRegistrationMu.RUnlock() + if !roastRetryRegistered { + return RoastRetryDeps{}, false + } + return roastRetryRegistration, true +} + +// ResetRoastRetryRegistrationForTest clears the registry. Exposed +// so tests in this and downstream packages can reset between cases +// without leaking state. Not intended for production code paths. +func ResetRoastRetryRegistrationForTest() { + roastRetryRegistrationMu.Lock() + defer roastRetryRegistrationMu.Unlock() + roastRetryRegistration = RoastRetryDeps{} + roastRetryRegistered = false +} diff --git a/pkg/frost/signing/roast_retry_registration_frost_roast_retry_test.go b/pkg/frost/signing/roast_retry_registration_frost_roast_retry_test.go new file mode 100644 index 0000000000..38130de9f2 --- /dev/null +++ b/pkg/frost/signing/roast_retry_registration_frost_roast_retry_test.go @@ -0,0 +1,97 @@ +//go:build frost_roast_retry + +package signing + +import ( + "sync" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast" +) + +func TestRoastRetryRegistration_TaggedBuildRoundTrip(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + if _, ok := RegisteredRoastRetryCoordinator(); ok { + t.Fatal("registry must start empty") + } + + coord := roast.NewInMemoryCoordinator() + deps := RoastRetryDeps{ + Coordinator: coord, + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 7, + } + RegisterRoastRetryCoordinator(deps) + + got, ok := RegisteredRoastRetryCoordinator() + if !ok { + t.Fatal("expected ok=true after register") + } + if got.SelfMember != 7 { + t.Fatalf("self member mismatch: got %d want 7", got.SelfMember) + } + if got.Coordinator == nil { + t.Fatal("coordinator must round-trip") + } +} + +func TestRoastRetryRegistration_LaterRegistrationOverwrites(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + RegisterRoastRetryCoordinator(RoastRetryDeps{SelfMember: 1}) + RegisterRoastRetryCoordinator(RoastRetryDeps{SelfMember: 2}) + got, ok := RegisteredRoastRetryCoordinator() + if !ok { + t.Fatal("expected ok=true after register") + } + if got.SelfMember != 2 { + t.Fatalf("later registration must win: got %d want 2", got.SelfMember) + } +} + +func TestRoastRetryRegistration_ResetClearsRegistry(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + RegisterRoastRetryCoordinator(RoastRetryDeps{SelfMember: 1}) + ResetRoastRetryRegistrationForTest() + if _, ok := RegisteredRoastRetryCoordinator(); ok { + t.Fatal("registry must be empty after reset") + } +} + +func TestRoastRetryRegistration_ConcurrentRegisterAndLookupIsRaceSafe(t *testing.T) { + ResetRoastRetryRegistrationForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + + var wg sync.WaitGroup + const registers = 32 + const lookups = 64 + for i := 0; i < registers; i++ { + wg.Add(1) + i := i + go func() { + defer wg.Done() + RegisterRoastRetryCoordinator(RoastRetryDeps{SelfMember: uint32(i + 1)}) + }() + } + for i := 0; i < lookups; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _, _ = RegisteredRoastRetryCoordinator() + }() + } + wg.Wait() + + // We don't assert a specific SelfMember -- registers race against + // each other and any of them can land last. We assert only that + // SOME registration succeeded. + if _, ok := RegisteredRoastRetryCoordinator(); !ok { + t.Fatal("expected at least one register to take effect") + } +} diff --git a/pkg/frost/signing/roast_retry_submit.go b/pkg/frost/signing/roast_retry_submit.go new file mode 100644 index 0000000000..3901e58214 --- /dev/null +++ b/pkg/frost/signing/roast_retry_submit.go @@ -0,0 +1,105 @@ +package signing + +import ( + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// roastRetryLogger is the logger the snapshot-submission path uses +// for non-fatal diagnostics (submission failures, signature errors). +// A submission failure does not propagate to the signing flow: +// Phase 4 ships the submission code path unused in production, and +// even when wired (Phase 5+) a transient submission failure is +// recoverable by the next attempt's evidence flow. +var roastRetryLogger = log.Logger("keep-frost-roast-retry") + +// submitSnapshotIfActive is invoked at end-of-collect to push the +// receive loop's accumulated evidence into the ROAST coordinator's +// RecordEvidence pipeline. The function is a no-op when any of the +// following is true: +// +// - the ROAST-retry registry is empty (default build, no caller +// has invoked RegisterRoastRetryCoordinator); +// - no session-handle binding exists for sessionID (the typical +// Phase-4 state, where the orchestration layer that calls +// SetCurrentAttemptHandleForSession is not yet implemented); +// - the recorder is a NoOp (no events were captured). +// +// When all three preconditions hold, the function builds a +// LocalEvidenceSnapshot, signs it with the registered Signer, and +// submits it via Coordinator.RecordEvidence. Errors at any step are +// logged at WARN level and otherwise swallowed -- snapshot +// submission must not break the receive loop's primary signing +// behaviour. +func submitSnapshotIfActive( + sessionID string, + recorder attempt.EvidenceRecorder, +) { + if recorder == nil { + return + } + deps, ok := RegisteredRoastRetryCoordinator() + if !ok { + return + } + handle, ctx, ok := currentAttemptHandleForCollect(sessionID) + if !ok { + return + } + evidence := recorder.Snapshot() + if len(evidence.Overflows) == 0 { + // Nothing observed worth submitting; emitting an empty + // snapshot is still meaningful in the ROAST protocol + // (proof-of-attendance) but adds noise to the bundle. + // Phase 4.3 chooses to skip empty submissions; Phase 5 + // orchestration may revisit this if attestations need to + // be unconditional. + return + } + snap := buildSignedSnapshot(deps, ctx, evidence) + if snap == nil { + return + } + if err := deps.Coordinator.RecordEvidence(handle, snap); err != nil { + roastRetryLogger.Warnf( + "roast-retry: RecordEvidence failed for session %q: %v", + sessionID, + err, + ) + } +} + +// buildSignedSnapshot constructs and signs a LocalEvidenceSnapshot +// from the captured evidence. Returns nil and logs on signature +// failure; callers treat nil as "skip submission" and continue. +func buildSignedSnapshot( + deps RoastRetryDeps, + ctx attempt.AttemptContext, + evidence attempt.Evidence, +) *roast.LocalEvidenceSnapshot { + snap := roast.NewLocalEvidenceSnapshot( + group.MemberIndex(deps.SelfMember), + ctx.Hash(), + evidence, + ) + payload, err := roast.CanonicalSnapshotBytes(snap) + if err != nil { + roastRetryLogger.Warnf( + "roast-retry: canonicalising snapshot failed: %v", + err, + ) + return nil + } + sig, err := deps.Signer.Sign(payload) + if err != nil { + roastRetryLogger.Warnf( + "roast-retry: signing snapshot failed: %v", + err, + ) + return nil + } + snap.OperatorSignature = sig + return snap +} diff --git a/pkg/frost/signing/roast_retry_submit_frost_roast_retry_test.go b/pkg/frost/signing/roast_retry_submit_frost_roast_retry_test.go new file mode 100644 index 0000000000..7e421a7963 --- /dev/null +++ b/pkg/frost/signing/roast_retry_submit_frost_roast_retry_test.go @@ -0,0 +1,318 @@ +//go:build frost_roast_retry + +package signing + +import ( + "errors" + "sync" + "testing" + + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// captureCoordinator is a roast.Coordinator wrapper that records +// every RecordEvidence call so tests can assert what was submitted. +// It delegates everything else to an embedded real coordinator. +type captureCoordinator struct { + inner roast.Coordinator + mu sync.Mutex + recordedFor []roast.AttemptHandle + recordedSnp []*roast.LocalEvidenceSnapshot + recordErr error +} + +func newCaptureCoordinator(inner roast.Coordinator) *captureCoordinator { + return &captureCoordinator{inner: inner} +} + +func (c *captureCoordinator) BeginAttempt(ctx attempt.AttemptContext) (roast.AttemptHandle, error) { + return c.inner.BeginAttempt(ctx) +} +func (c *captureCoordinator) State(h roast.AttemptHandle) (roast.AttemptState, error) { + return c.inner.State(h) +} +func (c *captureCoordinator) SelectedCoordinator(h roast.AttemptHandle) (group.MemberIndex, error) { + return c.inner.SelectedCoordinator(h) +} +func (c *captureCoordinator) RecordEvidence(h roast.AttemptHandle, s *roast.LocalEvidenceSnapshot) error { + c.mu.Lock() + defer c.mu.Unlock() + if c.recordErr != nil { + return c.recordErr + } + c.recordedFor = append(c.recordedFor, h) + c.recordedSnp = append(c.recordedSnp, s) + return c.inner.RecordEvidence(h, s) +} +func (c *captureCoordinator) AggregateBundle(h roast.AttemptHandle) (*roast.TransitionMessage, error) { + return c.inner.AggregateBundle(h) +} +func (c *captureCoordinator) VerifyBundle(h roast.AttemptHandle, m *roast.TransitionMessage) error { + return c.inner.VerifyBundle(h, m) +} +func (c *captureCoordinator) NextAttempt( + h roast.AttemptHandle, m *roast.TransitionMessage, t uint, pk []byte, +) (attempt.AttemptContext, error) { + return c.inner.NextAttempt(h, m, t, pk) +} + +// deterministicSigner produces SHA256(memberID || payload)-style +// signatures the captureSignatureVerifier accepts. +type deterministicSigner struct { + id group.MemberIndex +} + +func (d *deterministicSigner) Sign(payload []byte) ([]byte, error) { + out := make([]byte, len(payload)+1) + out[0] = byte(d.id) + copy(out[1:], payload) + return out, nil +} + +type deterministicVerifier struct{} + +func (deterministicVerifier) Verify( + payload []byte, signature []byte, signer group.MemberIndex, +) error { + if len(signature) != len(payload)+1 { + return errors.New("deterministicVerifier: length mismatch") + } + if signature[0] != byte(signer) { + return errors.New("deterministicVerifier: signer byte mismatch") + } + for i, b := range payload { + if signature[i+1] != b { + return errors.New("deterministicVerifier: payload byte mismatch") + } + } + return nil +} + +func newTestContextForSubmit(t *testing.T, sessionID string) attempt.AttemptContext { + t.Helper() + ctx, err := attempt.NewAttemptContext( + sessionID, + "key-group-submit", + []byte{0xAA}, + [attempt.MessageDigestLength]byte{0x42}, + 0, + []group.MemberIndex{1, 2, 3, 4, 5}, + nil, + ) + if err != nil { + t.Fatalf("ctx: %v", err) + } + return ctx +} + +func TestSubmitSnapshotIfActive_NoOpWhenRegistryEmpty(t *testing.T) { + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + // No registration, no binding. submit should be a no-op. + recorder := attempt.NewBoundedRecorder() + recorder.RecordOverflow(7) + submitSnapshotIfActive("session-x", recorder) + // Nothing to assert observably: success is the absence of a + // panic and no calls to a non-existent coordinator. +} + +func TestSubmitSnapshotIfActive_NoOpWhenSessionUnbound(t *testing.T) { + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + innerCoord := roast.NewInMemoryCoordinator() + cap := newCaptureCoordinator(innerCoord) + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: cap, + Signer: &deterministicSigner{id: 1}, + Verifier: deterministicVerifier{}, + SelfMember: 1, + }) + + recorder := attempt.NewBoundedRecorder() + recorder.RecordOverflow(7) + submitSnapshotIfActive("session-with-no-binding", recorder) + + if len(cap.recordedFor) != 0 { + t.Fatalf( + "expected no RecordEvidence calls when session unbound; got %d", + len(cap.recordedFor), + ) + } +} + +func TestSubmitSnapshotIfActive_NoOpWhenRecorderEmpty(t *testing.T) { + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + innerCoord := roast.NewInMemoryCoordinatorWithSigning( + 1, + &deterministicSigner{id: 1}, + deterministicVerifier{}, + ) + cap := newCaptureCoordinator(innerCoord) + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: cap, + Signer: &deterministicSigner{id: 1}, + Verifier: deterministicVerifier{}, + SelfMember: 1, + }) + + ctx := newTestContextForSubmit(t, "session-empty") + handle, err := cap.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + SetCurrentAttemptHandleForSession("session-empty", handle, ctx) + + // Recorder is bounded but has captured zero events. + recorder := attempt.NewBoundedRecorder() + submitSnapshotIfActive("session-empty", recorder) + + if len(cap.recordedFor) != 0 { + t.Fatalf( + "expected no RecordEvidence for empty snapshot; got %d", + len(cap.recordedFor), + ) + } +} + +func TestSubmitSnapshotIfActive_SubmitsSignedSnapshotWhenBoundAndPopulated(t *testing.T) { + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + const selfMember group.MemberIndex = 1 + innerCoord := roast.NewInMemoryCoordinatorWithSigning( + selfMember, + &deterministicSigner{id: selfMember}, + deterministicVerifier{}, + ) + cap := newCaptureCoordinator(innerCoord) + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: cap, + Signer: &deterministicSigner{id: selfMember}, + Verifier: deterministicVerifier{}, + SelfMember: uint32(selfMember), + }) + + ctx := newTestContextForSubmit(t, "session-real") + handle, err := cap.BeginAttempt(ctx) + if err != nil { + t.Fatalf("begin: %v", err) + } + SetCurrentAttemptHandleForSession("session-real", handle, ctx) + + recorder := attempt.NewBoundedRecorder() + recorder.RecordOverflow(3) + recorder.RecordOverflow(3) + recorder.RecordOverflow(5) + submitSnapshotIfActive("session-real", recorder) + + if len(cap.recordedFor) != 1 { + t.Fatalf("expected 1 RecordEvidence; got %d", len(cap.recordedFor)) + } + if cap.recordedFor[0] != handle { + t.Fatal("RecordEvidence handle mismatch") + } + snap := cap.recordedSnp[0] + if snap.SenderID() != selfMember { + t.Fatalf("snapshot sender: got %d want %d", snap.SenderID(), selfMember) + } + if len(snap.OperatorSignature) == 0 { + t.Fatal("snapshot must be signed") + } + // 2 distinct senders observed. + if len(snap.Overflows) != 2 { + t.Fatalf("expected 2 overflow entries; got %d", len(snap.Overflows)) + } +} + +func TestSetCurrentAttemptHandleForSession_LaterBindingOverwrites(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctxA := newTestContextForSubmit(t, "session-overwrite") + ctxB, _ := attempt.NewAttemptContext( + "session-overwrite", "key-group-submit", []byte{0xAA}, + [attempt.MessageDigestLength]byte{0x42}, 1, + []group.MemberIndex{1, 2, 3, 4, 5}, nil, + ) + h1 := roast.AttemptHandle{} + h2 := roast.AttemptHandle{} + + SetCurrentAttemptHandleForSession("session-overwrite", h1, ctxA) + gotHandle, gotCtx, ok := currentAttemptHandleForCollect("session-overwrite") + if !ok { + t.Fatal("expected binding after first Set") + } + if gotHandle != h1 { + t.Fatal("first binding handle mismatch") + } + if gotCtx.AttemptNumber != ctxA.AttemptNumber { + t.Fatal("first binding context mismatch") + } + + SetCurrentAttemptHandleForSession("session-overwrite", h2, ctxB) + _, gotCtx2, ok := currentAttemptHandleForCollect("session-overwrite") + if !ok { + t.Fatal("expected binding after second Set") + } + if gotCtx2.AttemptNumber != ctxB.AttemptNumber { + t.Fatal("second binding context did not overwrite first") + } +} + +func TestClearCurrentAttemptHandleForSession_RemovesBinding(t *testing.T) { + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetSessionHandleRegistryForTest) + + ctx := newTestContextForSubmit(t, "session-clear") + SetCurrentAttemptHandleForSession("session-clear", roast.AttemptHandle{}, ctx) + if _, _, ok := currentAttemptHandleForCollect("session-clear"); !ok { + t.Fatal("setup: binding must exist") + } + ClearCurrentAttemptHandleForSession("session-clear") + if _, _, ok := currentAttemptHandleForCollect("session-clear"); ok { + t.Fatal("binding must be cleared") + } +} + +func TestSubmitSnapshotIfActive_RecordEvidenceFailureIsLoggedNotPropagated(t *testing.T) { + ResetRoastRetryRegistrationForTest() + ResetSessionHandleRegistryForTest() + t.Cleanup(ResetRoastRetryRegistrationForTest) + t.Cleanup(ResetSessionHandleRegistryForTest) + + innerCoord := roast.NewInMemoryCoordinatorWithSigning( + 1, &deterministicSigner{id: 1}, deterministicVerifier{}, + ) + cap := newCaptureCoordinator(innerCoord) + cap.recordErr = errors.New("synthetic RecordEvidence failure") + RegisterRoastRetryCoordinator(RoastRetryDeps{ + Coordinator: cap, + Signer: &deterministicSigner{id: 1}, + Verifier: deterministicVerifier{}, + SelfMember: 1, + }) + + ctx := newTestContextForSubmit(t, "session-failure") + handle, _ := cap.BeginAttempt(ctx) + SetCurrentAttemptHandleForSession("session-failure", handle, ctx) + + recorder := attempt.NewBoundedRecorder() + recorder.RecordOverflow(3) + + // Must not panic. Caller is unaffected. + submitSnapshotIfActive("session-failure", recorder) +} diff --git a/pkg/frost/signing/signing.go b/pkg/frost/signing/signing.go new file mode 100644 index 0000000000..3ea4ab3a63 --- /dev/null +++ b/pkg/frost/signing/signing.go @@ -0,0 +1,121 @@ +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" +) + +// 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, + channel net.BroadcastChannel, + membershipValidator *group.MembershipValidator, + attempt *Attempt, +) (*Result, error) { + request := &Request{ + Message: message, + SessionID: sessionID, + MemberIndex: memberIndex, + SignerMaterial: privateKeyShare, + PrivateKeyShare: privateKeyShare, + GroupSize: groupSize, + DishonestThreshold: dishonestThreshold, + Channel: channel, + MembershipValidator: membershipValidator, + 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, + &clonedRequest, + ) +} + +// RegisterUnmarshallers initializes all required message unmarshallers. +// For now, signing transport message formats are delegated to the legacy +// engine implementation. +func RegisterUnmarshallers(channel net.BroadcastChannel) { + currentExecutionBackend().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..f54ff8cfe4 --- /dev/null +++ b/pkg/frost/signing/signing_test.go @@ -0,0 +1,183 @@ +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" +) + +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") + } + }) + } +} + +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/frost/signing/unsupported_uniffi_v2_test.go b/pkg/frost/signing/unsupported_uniffi_v2_test.go new file mode 100644 index 0000000000..3cc65680ab --- /dev/null +++ b/pkg/frost/signing/unsupported_uniffi_v2_test.go @@ -0,0 +1,32 @@ +//go:build frost_native + +package signing + +import "encoding/json" + +func unsupportedUniFFIV2Payload(t testFataler, verifyingKey string) []byte { + t.Helper() + + payload, err := json.Marshal(&struct { + KeyPackage *NativeFROSTKeyPackage `json:"keyPackage"` + PublicKeyPackage *NativeFROSTPublicKeyPackage `json:"publicKeyPackage"` + }{ + KeyPackage: &NativeFROSTKeyPackage{ + Identifier: "id-1", + Data: []byte{0x01}, + }, + PublicKeyPackage: &NativeFROSTPublicKeyPackage{ + VerifyingKey: verifyingKey, + }, + }) + if err != nil { + t.Fatalf("marshal: %v", err) + } + + return payload +} + +type testFataler interface { + Helper() + Fatalf(format string, args ...any) +} diff --git a/pkg/frost/testdata/wallet-pubkey-hash-derivation-vectors-v1.json b/pkg/frost/testdata/wallet-pubkey-hash-derivation-vectors-v1.json new file mode 100644 index 0000000000..841e66180d --- /dev/null +++ b/pkg/frost/testdata/wallet-pubkey-hash-derivation-vectors-v1.json @@ -0,0 +1,71 @@ +{ + "name": "wallet-pubkey-hash-derivation-vectors", + "version": "v1", + "description": "Cross-repo test vectors for HASH160-based wallet pubkey hash derivation. Both the tbtc bridge contracts (tlabs-xyz/tbtc) and the keep-core FROST protocol (threshold-network/keep-core) must derive the same 20-byte alias from the same input. Drift between the two derivations silently breaks the bridge-protocol identity contract for any wallet whose canonical identity is established cross-repo. This fixture is the tripwire: identical JSON is checked into both repos; each side has a test that reads it and asserts its own derivation function reproduces the expected output. If the two sides diverge, at least one repo's test fails.", + "ecdsa_legacy": [ + { + "name": "secp256k1 generator point (compressed, even y)", + "input": { + "compressedPubKey": "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + }, + "expected": { + "walletPubKeyHash": "0x751e76e8199196d454941c45d1b3a323f1433bd6" + }, + "note": "Bitcoin's classic generator-point compressed pubkey, well-known HASH160. Corresponds to mainnet address 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2." + }, + { + "name": "Near-zero scalar pubkey (compressed, even y)", + "input": { + "compressedPubKey": "0x02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + }, + "expected": { + "walletPubKeyHash": "0x06afd46bcdfd22ef94ac122aa11f241244a37ecc" + } + }, + { + "name": "tBTC fixture pubkey (matches contracts/tbtc-v2/test/data/ecdsa.ts)", + "input": { + "compressedPubKey": "0x0250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352" + }, + "expected": { + "walletPubKeyHash": "0xf54a5851e9372b87810a8e60cdd2e7cfd80b6e31" + }, + "note": "Cross-validates against the existing pubKeyHash160 constant in the tBTC ECDSA test fixture data, ensuring this vector matches what the tBTC test suite already pins." + } + ], + "frost_p2tr": [ + { + "name": "Representative FROST x-only output key", + "input": { + "xOnlyOutputKey": "0xb1de1afa17e1cbb20d8a4f8e54f8a55fbf5c8d2da9e1c6c4d1f0c7b3a2e5d4c8" + }, + "expected": { + "walletPubKeyHash": "0xac756e3ad02acf580218a3ba2232b081906be776" + }, + "note": "The high 12 bytes are non-zero, matching the native-shape constraint required by the FROST wallet registration entry point (see PR #431). The expected pubKeyHash is HASH160(0x02 || xOnlyOutputKey)." + }, + { + "name": "All-ones x-only key (regression case)", + "input": { + "xOnlyOutputKey": "0x0101010101010101010101010101010101010101010101010101010101010101" + }, + "expected": { + "walletPubKeyHash": "0x9b596d772a3bfe0335f36c38357f026221212c90" + } + }, + { + "name": "All-max x-only key (boundary case)", + "input": { + "xOnlyOutputKey": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "expected": { + "walletPubKeyHash": "0x2914980c04dec23ab03cfcd610adf39d62d7c5fb" + } + } + ], + "drift_check": { + "tbtc_path": "docs/test-vectors/wallet-pubkey-hash-derivation-vectors-v1.json", + "keep_core_path": "pkg/frost/testdata/wallet-pubkey-hash-derivation-vectors-v1.json", + "rule": "The byte-identical JSON file must exist at both paths. A future CI check should compare file hashes between repos; for now, the per-repo tests catch derivation drift even if the JSON itself drifts (the harder failure mode is silently identical JSON with different implementations underneath)." + } +} diff --git a/pkg/frost/types.go b/pkg/frost/types.go new file mode 100644 index 0000000000..02399033b8 --- /dev/null +++ b/pkg/frost/types.go @@ -0,0 +1,96 @@ +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 + // 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. +type OutputKey [OutputKeySize]byte + +// WalletPublicKeyHashCompatibilityAlias computes the 20-byte compatibility +// alias from a Taproot output key: +// HASH160(0x02 || xOnlyOutputKey). +// The x-only output key is assumed to already use BIP-340's even-Y convention; +// callers must not strip a compressed odd-Y key without first applying the +// BIP-340 negation rule upstream. +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 [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() + 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..ef3cbdb520 --- /dev/null +++ b/pkg/frost/types_test.go @@ -0,0 +1,94 @@ +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 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}, + 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, + ) + } +} diff --git a/pkg/frost/wallet_pubkey_hash_derivation_vectors_test.go b/pkg/frost/wallet_pubkey_hash_derivation_vectors_test.go new file mode 100644 index 0000000000..0946d08c6b --- /dev/null +++ b/pkg/frost/wallet_pubkey_hash_derivation_vectors_test.go @@ -0,0 +1,263 @@ +package frost + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "golang.org/x/crypto/ripemd160" //nolint:staticcheck // RIPEMD-160 is intentional for the HASH160 derivation. +) + +// Cross-repo derivation fixture (also checked into the tbtc bridge repo +// at docs/test-vectors/wallet-pubkey-hash-derivation-vectors-v1.json). +// Each repo's test must reproduce the expected output from the same +// input; if either side drifts from the other, at least one repo's +// test fails. Drift between bridge and keep-core silently breaks the +// wallet identity contract for any wallet whose canonical identity is +// established cross-repo (in particular, FROST wallets registered via +// the FROST WalletRegistry will use this derivation). +// +// Path constants follow two different conventions intentionally: +// +// - walletPubKeyHashDerivationVectorsTestPath: package-relative, +// used by os.ReadFile() because `go test ./pkg/frost` runs with +// pkg/frost as the working directory. This is the standard Go +// testdata convention. +// +// - walletPubKeyHashDerivationVectorsRepoPath: repo-root-relative, +// used to compare against fixture.DriftCheck.KeepCorePath +// (which declares the canonical location for cross-repo sync +// tooling). This is what a cross-repo diff tool would use. +// +// The two MUST refer to the same file; the TestDriftCheckMetadata +// assertions verify the fixture's self-declared path matches the +// repo-relative constant exactly. +const ( + walletPubKeyHashDerivationVectorsTestPath = "testdata/wallet-pubkey-hash-derivation-vectors-v1.json" + walletPubKeyHashDerivationVectorsRepoPath = "pkg/frost/testdata/wallet-pubkey-hash-derivation-vectors-v1.json" +) + +type ecdsaVector struct { + Name string `json:"name"` + Input struct { + CompressedPubKey string `json:"compressedPubKey"` + } `json:"input"` + Expected struct { + WalletPubKeyHash string `json:"walletPubKeyHash"` + } `json:"expected"` + Note string `json:"note,omitempty"` +} + +type frostVector struct { + Name string `json:"name"` + Input struct { + XOnlyOutputKey string `json:"xOnlyOutputKey"` + } `json:"input"` + Expected struct { + WalletPubKeyHash string `json:"walletPubKeyHash"` + } `json:"expected"` + Note string `json:"note,omitempty"` +} + +type derivationFixture struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + EcdsaLegacy []ecdsaVector `json:"ecdsa_legacy"` + FrostP2tr []frostVector `json:"frost_p2tr"` + DriftCheck struct { + TbtcPath string `json:"tbtc_path"` + KeepCorePath string `json:"keep_core_path"` + Rule string `json:"rule"` + } `json:"drift_check"` +} + +func loadDerivationFixture(t *testing.T) derivationFixture { + t.Helper() + + data, err := os.ReadFile(walletPubKeyHashDerivationVectorsTestPath) + if err != nil { + t.Fatalf("fixture read: %v", err) + } + var fixture derivationFixture + if err := json.Unmarshal(data, &fixture); err != nil { + t.Fatalf("fixture parse: %v", err) + } + if fixture.Version != "v1" { + t.Fatalf( + "fixture schemaVersion drift: got %q, expected %q -- both repos must update together", + fixture.Version, + "v1", + ) + } + return fixture +} + +// TestFrostWalletPubKeyHashDerivationVectors checks that +// frost.WalletPublicKeyHashCompatibilityAlias produces the expected +// 20-byte HASH160(0x02 || xOnlyOutputKey) for every FROST vector in +// the shared cross-repo fixture. The tbtc bridge runs the equivalent +// check against its own derivation (BitcoinTx.deriveWalletPubKeyHash- +// FromXOnly); if either side drifts, the wallet identity contract +// between the bridge and the protocol silently breaks for any FROST +// wallet whose canonical identity is established cross-repo. +func TestFrostWalletPubKeyHashDerivationVectors(t *testing.T) { + fixture := loadDerivationFixture(t) + + if len(fixture.FrostP2tr) == 0 { + t.Fatal("fixture must contain at least one FROST vector") + } + + for _, vector := range fixture.FrostP2tr { + vector := vector + t.Run(vector.Name, func(t *testing.T) { + xOnlyBytes, err := hex.DecodeString( + strings.TrimPrefix(vector.Input.XOnlyOutputKey, "0x"), + ) + if err != nil { + t.Fatalf("decode xOnlyOutputKey: %v", err) + } + if len(xOnlyBytes) != OutputKeySize { + t.Fatalf( + "xOnlyOutputKey length: got %d, expected %d", + len(xOnlyBytes), + OutputKeySize, + ) + } + + var outputKey OutputKey + copy(outputKey[:], xOnlyBytes) + + alias := WalletPublicKeyHashCompatibilityAlias(outputKey) + got := "0x" + hex.EncodeToString(alias[:]) + want := strings.ToLower(vector.Expected.WalletPubKeyHash) + + if got != want { + t.Fatalf( + "derivation drift for vector %q:\n got: %s\n want: %s\n"+ + "\nThis test enforces the cross-repo contract that\n"+ + "frost.WalletPublicKeyHashCompatibilityAlias and the\n"+ + "tbtc bridge's BitcoinTx.deriveWalletPubKeyHashFromXOnly\n"+ + "produce the same 20-byte alias for the same input.\n"+ + "If this test fails, also expect the tbtc-side test to\n"+ + "fail unless the JSON fixture itself has drifted.", + vector.Name, + got, + want, + ) + } + }) + } +} + +// TestEcdsaCompressedPubKeyHash160Vectors checks the legacy ECDSA +// derivation path: HASH160 of the compressed pubkey. The tbtc bridge +// performs this implicitly during registerNewWallet (compress then +// hash160). The off-chain operator tooling that produces deposit +// scripts performs the same derivation; this test pins the algorithm +// from the keep-core side using the same vectors the bridge pins on +// its side. +func TestEcdsaCompressedPubKeyHash160Vectors(t *testing.T) { + fixture := loadDerivationFixture(t) + + if len(fixture.EcdsaLegacy) == 0 { + t.Fatal("fixture must contain at least one ECDSA vector") + } + + for _, vector := range fixture.EcdsaLegacy { + vector := vector + t.Run(vector.Name, func(t *testing.T) { + compressed, err := hex.DecodeString( + strings.TrimPrefix(vector.Input.CompressedPubKey, "0x"), + ) + if err != nil { + t.Fatalf("decode compressedPubKey: %v", err) + } + + got := "0x" + hex.EncodeToString(hash160(compressed)) + want := strings.ToLower(vector.Expected.WalletPubKeyHash) + + if got != want { + t.Fatalf( + "HASH160 drift for vector %q:\n got: %s\n want: %s", + vector.Name, + got, + want, + ) + } + }) + } +} + +// TestDriftCheckMetadata asserts the fixture declares the tbtc mirror +// path and a non-empty drift rule. A future CI sync check can use +// these fields to compare files between repos. The fixture's +// keep_core_path is repo-root-relative by convention; the package- +// relative testdata constant used by os.ReadFile() is a separate +// representation of the same file. +func TestDriftCheckMetadata(t *testing.T) { + fixture := loadDerivationFixture(t) + + if fixture.DriftCheck.TbtcPath != "docs/test-vectors/wallet-pubkey-hash-derivation-vectors-v1.json" { + t.Errorf( + "drift_check.tbtc_path drift: got %q", + fixture.DriftCheck.TbtcPath, + ) + } + if fixture.DriftCheck.KeepCorePath != walletPubKeyHashDerivationVectorsRepoPath { + t.Errorf( + "drift_check.keep_core_path drift: fixture says %q, repo convention is %q", + fixture.DriftCheck.KeepCorePath, + walletPubKeyHashDerivationVectorsRepoPath, + ) + } + if fixture.DriftCheck.Rule == "" { + t.Error("drift_check.rule must be non-empty") + } +} + +// TestFixtureFileShouldExistAtMirrorPath documents the convention that +// the file lives at the path the fixture self-declares. Since the +// fixture's keep_core_path is repo-root-relative but `go test +// ./pkg/frost` runs with pkg/frost as the working directory, the path +// is resolved relative to the repo root by walking up from this test +// file's location. +func TestFixtureFileShouldExistAtMirrorPath(t *testing.T) { + fixture := loadDerivationFixture(t) + + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("runtime.Caller: cannot locate test source file") + } + // thisFile points at pkg/frost/wallet_pubkey_hash_derivation_vectors_test.go + // repo root is two directories up. + repoRoot := filepath.Clean( + filepath.Join(filepath.Dir(thisFile), "..", ".."), + ) + abs := filepath.Join(repoRoot, fixture.DriftCheck.KeepCorePath) + if _, err := os.Stat(abs); err != nil { + t.Fatalf( + "fixture self-declares it lives at %q (resolved to %q) but the file is not there: %v", + fixture.DriftCheck.KeepCorePath, + abs, + err, + ) + } +} + +// hash160 reproduces Bitcoin's HASH160 (RIPEMD160(SHA256(x))) using +// the same primitive frost.WalletPublicKeyHashCompatibilityAlias +// invokes via btcutil.Hash160. We compute it directly here so the +// ECDSA test is self-contained and doesn't pull in btcutil for a one- +// liner. +func hash160(b []byte) []byte { + sha := sha256.Sum256(b) + rip := ripemd160.New() + rip.Write(sha[:]) + return rip.Sum(nil) +} diff --git a/pkg/generator/scheduler.go b/pkg/generator/scheduler.go index 73c9d25350..014f7b1395 100644 --- a/pkg/generator/scheduler.go +++ b/pkg/generator/scheduler.go @@ -112,10 +112,16 @@ 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, cancelFn) + s.stops = append(s.stops, func() { + cancelFn() + }) go func() { + defer cancelFn() + for { select { case <-ctx.Done(): diff --git a/pkg/maintainer/spv/bitcoin_chain_test.go b/pkg/maintainer/spv/bitcoin_chain_test.go index 2f790bf11f..447c2ca005 100644 --- a/pkg/maintainer/spv/bitcoin_chain_test.go +++ b/pkg/maintainer/spv/bitcoin_chain_test.go @@ -148,6 +148,38 @@ func (lbc *localBitcoinChain) GetTransactionsForPublicKeyHash( return matchingTransactions, nil } +func (lbc *localBitcoinChain) GetTransactionsForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, + limit int, +) ([]*bitcoin.Transaction, error) { + lbc.mutex.Lock() + defer lbc.mutex.Unlock() + + matchingTransactions := make([]*bitcoin.Transaction, 0) + + for _, transaction := range lbc.transactions { + transactionMatches := false + for _, output := range transaction.Outputs { + for _, publicKeyScript := range publicKeyScripts { + if bytes.Equal(output.PublicKeyScript, publicKeyScript) { + matchingTransactions = append(matchingTransactions, transaction) + transactionMatches = true + break + } + } + if transactionMatches { + break + } + } + } + + if len(matchingTransactions) > limit { + return matchingTransactions[len(matchingTransactions)-limit:], nil + } + + return matchingTransactions, nil +} + func (lbc *localBitcoinChain) GetTxHashesForPublicKeyHash( publicKeyHash [20]byte, ) ([]bitcoin.Hash, error) { diff --git a/pkg/maintainer/spv/chain.go b/pkg/maintainer/spv/chain.go index c9e060e9bf..a3444fd349 100644 --- a/pkg/maintainer/spv/chain.go +++ b/pkg/maintainer/spv/chain.go @@ -32,6 +32,10 @@ type Chain interface { walletPublicKeyHash [20]byte, ) (*tbtc.WalletChainData, error) + // WalletPublicKeyHashForWalletID resolves the canonical wallet ID to the + // wallet public key hash used by Bridge mappings. + WalletPublicKeyHashForWalletID(walletID [32]byte) ([20]byte, error) + // ComputeMainUtxoHash computes the hash of the provided main UTXO // according to the on-chain Bridge rules. ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte diff --git a/pkg/maintainer/spv/chain_test.go b/pkg/maintainer/spv/chain_test.go index 2a0bcc0a89..99cb4b4013 100644 --- a/pkg/maintainer/spv/chain_test.go +++ b/pkg/maintainer/spv/chain_test.go @@ -163,6 +163,28 @@ func (lc *localChain) GetWallet(walletPublicKeyHash [20]byte) ( return walletChainData, nil } +func (lc *localChain) WalletPublicKeyHashForWalletID( + walletID [32]byte, +) ([20]byte, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + for walletPublicKeyHash, walletChainData := range lc.wallets { + if walletChainData.WalletID == walletID || + walletChainData.EcdsaWalletID == walletID { + return walletPublicKeyHash, nil + } + } + + legacyWalletPublicKeyHash, ok := + tbtc.WalletPublicKeyHashFromLegacyWalletID(walletID) + if ok { + return legacyWalletPublicKeyHash, nil + } + + return [20]byte{}, fmt.Errorf("no wallet for given wallet ID") +} + func (lc *localChain) setWallet( walletPublicKeyHash [20]byte, walletChainData *tbtc.WalletChainData, diff --git a/pkg/maintainer/spv/deposit_sweep.go b/pkg/maintainer/spv/deposit_sweep.go index 2b0b8a5f77..4e1d52eb8f 100644 --- a/pkg/maintainer/spv/deposit_sweep.go +++ b/pkg/maintainer/spv/deposit_sweep.go @@ -6,7 +6,6 @@ import ( "github.com/keep-network/keep-core/pkg/tbtc" - "github.com/btcsuite/btcd/txscript" "github.com/ethereum/go-ethereum/common" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" @@ -156,13 +155,25 @@ func parseDepositSweepTransactionInputs( publicKeyScript := previousTransaction.Outputs[outpointIndex].PublicKeyScript value := previousTransaction.Outputs[outpointIndex].Value - scriptClass := txscript.GetScriptClass(publicKeyScript) + scriptType := bitcoin.GetScriptType(publicKeyScript) - if scriptClass == txscript.PubKeyHashTy || - scriptClass == txscript.WitnessV0PubKeyHashTy { - // The input is P2PKH or P2WPKH, so we found main UTXO. There should - // be at most one main UTXO. If any input of this kind has already - // been found, report an error. + deposit, found, err := spvChain.GetDepositRequest( + outpointTransactionHash, + outpointIndex, + ) + if err != nil { + return bitcoin.UnspentTransactionOutput{}, common.Address{}, fmt.Errorf( + "failed to get deposit request: [%v]", + err, + ) + } + + if !found && (scriptType == bitcoin.P2PKHScript || + scriptType == bitcoin.P2WPKHScript || + scriptType == bitcoin.P2TRScript) { + // The input is a direct wallet UTXO, so we found main UTXO. + // There should be at most one main UTXO. If any input of this + // kind has already been found, report an error. if mainUTXO == nil { mainUTXO = &bitcoin.UnspentTransactionOutput{ Outpoint: &bitcoin.TransactionOutpoint{ @@ -177,31 +188,13 @@ func parseDepositSweepTransactionInputs( "inputs", ) } - } else if scriptClass == txscript.ScriptHashTy || - scriptClass == txscript.WitnessV0ScriptHashTy { - // The input is P2SH or P2WSH, so we found a deposit input. All + } else if found && (scriptType == bitcoin.P2SHScript || + scriptType == bitcoin.P2WSHScript || + scriptType == bitcoin.P2TRScript) { + // The input is a deposit input. All // the deposits should have the same vault set or no vault at all. // If the vault if different than the vault from any previous // deposit input, report an error. - deposit, found, err := spvChain.GetDepositRequest( - outpointTransactionHash, - outpointIndex, - ) - if err != nil { - return bitcoin.UnspentTransactionOutput{}, common.Address{}, fmt.Errorf( - "failed to get deposit request: [%v]", - err, - ) - } - - if !found { - return bitcoin.UnspentTransactionOutput{}, common.Address{}, fmt.Errorf( - "deposit: [%v/%v] not found", - outpointTransactionHash, - outpointIndex, - ) - } - if depositAlreadyProcessed { if vault != convertVaultAddress(deposit.Vault) { return bitcoin.UnspentTransactionOutput{}, common.Address{}, fmt.Errorf( diff --git a/pkg/maintainer/spv/deposit_sweep_test.go b/pkg/maintainer/spv/deposit_sweep_test.go index dc61256ccf..69cf92736e 100644 --- a/pkg/maintainer/spv/deposit_sweep_test.go +++ b/pkg/maintainer/spv/deposit_sweep_test.go @@ -134,6 +134,193 @@ func TestSubmitDepositSweepProof(t *testing.T) { ) } +func TestParseDepositSweepTransactionInputs_TaprootDeposit(t *testing.T) { + btcChain := newLocalBitcoinChain() + spvChain := newLocalChain() + + taprootDepositScript, err := bitcoin.PayToTaproot([32]byte{0x01}) + if err != nil { + t.Fatal(err) + } + + walletScript, err := bitcoin.PayToTaproot([32]byte{0x02}) + if err != nil { + t.Fatal(err) + } + + depositTransaction := &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 800000, + PublicKeyScript: taprootDepositScript, + }, + }, + } + + if err := btcChain.BroadcastTransaction(depositTransaction); err != nil { + t.Fatal(err) + } + + spvChain.setDepositRequest( + depositTransaction.Hash(), + 0, + &tbtc.DepositChainRequest{ + RevealedAt: time.Unix(1000, 0), + SweptAt: time.Unix(0, 0), + }, + ) + + depositSweepTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: depositTransaction.Hash(), + OutputIndex: 0, + }, + Witness: [][]byte{make([]byte, 64)}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 799000, + PublicKeyScript: walletScript, + }, + }, + } + + mainUTXO, vault, err := parseDepositSweepTransactionInputs( + btcChain, + spvChain, + depositSweepTransaction, + ) + if err != nil { + t.Fatal(err) + } + + expectedMainUtxo := bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{}, + OutputIndex: 0, + }, + Value: 0, + } + if diff := deep.Equal(expectedMainUtxo, mainUTXO); diff != nil { + t.Errorf("invalid main UTXO: %v", diff) + } + + expectedVault := [20]byte{} + testutils.AssertBytesEqual(t, expectedVault[:], vault[:]) +} + +func TestParseDepositSweepTransactionInputs_TaprootMainUtxoAndDeposit( + t *testing.T, +) { + btcChain := newLocalBitcoinChain() + spvChain := newLocalChain() + + walletScript, err := bitcoin.PayToTaproot([32]byte{0x01}) + if err != nil { + t.Fatal(err) + } + + taprootDepositScript, err := bitcoin.PayToTaproot([32]byte{0x02}) + if err != nil { + t.Fatal(err) + } + + mainUtxoTransaction := &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 700000, + PublicKeyScript: walletScript, + }, + }, + } + + depositTransaction := &bitcoin.Transaction{ + Version: 2, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 800000, + PublicKeyScript: taprootDepositScript, + }, + }, + } + + for _, transaction := range []*bitcoin.Transaction{ + mainUtxoTransaction, + depositTransaction, + } { + if err := btcChain.BroadcastTransaction(transaction); err != nil { + t.Fatal(err) + } + } + + spvChain.setDepositRequest( + depositTransaction.Hash(), + 0, + &tbtc.DepositChainRequest{ + RevealedAt: time.Unix(1000, 0), + SweptAt: time.Unix(0, 0), + }, + ) + + depositSweepTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: mainUtxoTransaction.Hash(), + OutputIndex: 0, + }, + Witness: [][]byte{make([]byte, 64)}, + Sequence: 0xffffffff, + }, + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: depositTransaction.Hash(), + OutputIndex: 0, + }, + Witness: [][]byte{make([]byte, 64)}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1499000, + PublicKeyScript: walletScript, + }, + }, + } + + mainUTXO, vault, err := parseDepositSweepTransactionInputs( + btcChain, + spvChain, + depositSweepTransaction, + ) + if err != nil { + t.Fatal(err) + } + + expectedMainUtxo := bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: mainUtxoTransaction.Hash(), + OutputIndex: 0, + }, + Value: 700000, + } + if diff := deep.Equal(expectedMainUtxo, mainUTXO); diff != nil { + t.Errorf("invalid main UTXO: %v", diff) + } + + expectedVault := [20]byte{} + testutils.AssertBytesEqual(t, expectedVault[:], vault[:]) +} + func TestGetUnprovenDepositSweepTransactions(t *testing.T) { bytesFromHex := func(str string) []byte { value, err := hex.DecodeString(str) diff --git a/pkg/maintainer/spv/redemptions.go b/pkg/maintainer/spv/redemptions.go index e504860f81..4694d7665b 100644 --- a/pkg/maintainer/spv/redemptions.go +++ b/pkg/maintainer/spv/redemptions.go @@ -75,6 +75,7 @@ func submitRedemptionProof( } mainUTXO, walletPublicKeyHash, err := parseRedemptionTransactionInput( + spvChain, btcChain, transaction, ) @@ -114,6 +115,7 @@ func submitRedemptionProof( // parseRedemptionTransactionInput parses the transaction's input and // returns the main UTXO and the wallet public key hash. func parseRedemptionTransactionInput( + spvChain Chain, btcChain bitcoin.Chain, transaction *bitcoin.Transaction, ) (bitcoin.UnspentTransactionOutput, [20]byte, error) { @@ -146,18 +148,127 @@ func parseRedemptionTransactionInput( Value: spentOutput.Value, } - // Extract the wallet public key hash from script - walletPublicKeyHash, err := bitcoin.ExtractPublicKeyHash(spentOutput.PublicKeyScript) + walletPublicKeyHash, err := walletPublicKeyHashFromScript( + spvChain, + spentOutput.PublicKeyScript, + ) if err != nil { - return bitcoin.UnspentTransactionOutput{}, [20]byte{}, fmt.Errorf( - "cannot extract wallet public key hash: [%v]", - err, - ) + return bitcoin.UnspentTransactionOutput{}, [20]byte{}, err } return mainUtxo, walletPublicKeyHash, nil } +func walletPublicKeyHashFromScript( + spvChain Chain, + script bitcoin.Script, +) ([20]byte, error) { + switch bitcoin.GetScriptType(script) { + case bitcoin.P2PKHScript, bitcoin.P2WPKHScript: + walletPublicKeyHash, err := bitcoin.ExtractPublicKeyHash(script) + if err != nil { + return [20]byte{}, fmt.Errorf( + "cannot extract wallet public key hash: [%v]", + err, + ) + } + + return walletPublicKeyHash, nil + case bitcoin.P2TRScript: + walletID, err := bitcoin.ExtractTaprootKey(script) + if err != nil { + return [20]byte{}, fmt.Errorf( + "cannot extract wallet Taproot key: [%v]", + err, + ) + } + + walletPublicKeyHash, err := + spvChain.WalletPublicKeyHashForWalletID(walletID) + if err != nil { + return [20]byte{}, fmt.Errorf( + "cannot resolve wallet public key hash for Taproot wallet ID "+ + "[0x%x]: [%v]", + walletID, + err, + ) + } + + return walletPublicKeyHash, nil + default: + return [20]byte{}, fmt.Errorf( + "not a wallet public key hash or Taproot script", + ) + } +} + +type transactionsForPublicKeyScriptsChain interface { + GetTransactionsForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, + limit int, + ) ([]*bitcoin.Transaction, error) +} + +func getWalletTransactions( + walletPublicKeyHash [20]byte, + wallet *tbtc.WalletChainData, + transactionLimit int, + btcChain bitcoin.Chain, +) ([]*bitcoin.Transaction, error) { + scriptChain, ok := btcChain.(transactionsForPublicKeyScriptsChain) + if !ok { + return btcChain.GetTransactionsForPublicKeyHash( + walletPublicKeyHash, + transactionLimit, + ) + } + + publicKeyScripts, err := walletPublicKeyScripts( + walletPublicKeyHash, + wallet, + ) + if err != nil { + return nil, err + } + + return scriptChain.GetTransactionsForPublicKeyScripts( + publicKeyScripts, + transactionLimit, + ) +} + +func walletPublicKeyScripts( + walletPublicKeyHash [20]byte, + wallet *tbtc.WalletChainData, +) ([]bitcoin.Script, error) { + p2pkh, err := bitcoin.PayToPublicKeyHash(walletPublicKeyHash) + if err != nil { + return nil, fmt.Errorf("cannot construct P2PKH for wallet: [%v]", err) + } + + p2wpkh, err := bitcoin.PayToWitnessPublicKeyHash(walletPublicKeyHash) + if err != nil { + return nil, fmt.Errorf("cannot construct P2WPKH for wallet: [%v]", err) + } + + publicKeyScripts := []bitcoin.Script{p2pkh, p2wpkh} + + if wallet.WalletID != [32]byte{} { + // FROST Taproot wallets use the canonical wallet ID as the x-only + // Taproot output key. Legacy wallets will not normally have history + // under this script, but querying it is harmless and keeps discovery + // independent of wallet generation. + p2tr, err := bitcoin.PayToTaproot(wallet.WalletID) + if err != nil { + return nil, fmt.Errorf("cannot construct P2TR for wallet: [%v]", err) + } + + publicKeyScripts = append(publicKeyScripts, p2tr) + } + + return publicKeyScripts, nil +} + func getUnprovenRedemptionTransactions( historyDepth uint64, transactionLimit int, @@ -221,9 +332,11 @@ func getUnprovenRedemptionTransactions( continue } - walletTransactions, err := btcChain.GetTransactionsForPublicKeyHash( + walletTransactions, err := getWalletTransactions( walletPublicKeyHash, + wallet, transactionLimit, + btcChain, ) if err != nil { return nil, fmt.Errorf( @@ -305,7 +418,11 @@ func isUnprovenRedemptionTransaction( // First, check if the given output is a change (if it wasn't // found yet). if !changeFound { - isChange, err := isWalletChangeOutput(walletPublicKeyHash, output) + isChange, err := isWalletChangeOutput( + walletPublicKeyHash, + spvChain, + output, + ) if err != nil { return false, fmt.Errorf( "failed to check if output is wallet change: [%v]", @@ -351,6 +468,7 @@ func isUnprovenRedemptionTransaction( func isWalletChangeOutput( walletPublicKeyHash [20]byte, + spvChain Chain, output *bitcoin.TransactionOutput, ) (bool, error) { walletP2PKH, err := bitcoin.PayToPublicKeyHash(walletPublicKeyHash) @@ -363,5 +481,26 @@ func isWalletChangeOutput( } script := output.PublicKeyScript - return bytes.Equal(script, walletP2PKH) || bytes.Equal(script, walletP2WPKH), nil + if bytes.Equal(script, walletP2PKH) || bytes.Equal(script, walletP2WPKH) { + return true, nil + } + + if bitcoin.GetScriptType(script) != bitcoin.P2TRScript { + return false, nil + } + + wallet, err := spvChain.GetWallet(walletPublicKeyHash) + if err != nil { + return false, fmt.Errorf("cannot get wallet: [%v]", err) + } + if wallet.WalletID == [32]byte{} { + return false, nil + } + + walletP2TR, err := bitcoin.PayToTaproot(wallet.WalletID) + if err != nil { + return false, fmt.Errorf("cannot construct P2TR for wallet: [%v]", err) + } + + return bytes.Equal(script, walletP2TR), nil } diff --git a/pkg/maintainer/spv/redemptions_test.go b/pkg/maintainer/spv/redemptions_test.go index 4f10a3a208..f019a7c9de 100644 --- a/pkg/maintainer/spv/redemptions_test.go +++ b/pkg/maintainer/spv/redemptions_test.go @@ -112,6 +112,140 @@ func TestSubmitRedemptionProof(t *testing.T) { testutils.AssertBytesEqual(t, bytesFromHex("03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e"), submittedProof.walletPublicKeyHash[:]) } +func TestSubmitRedemptionProof_TaprootMainUtxo(t *testing.T) { + bytesFromHex := func(str string) []byte { + value, err := hex.DecodeString(str) + if err != nil { + t.Fatal(err) + } + + return value + } + + bytes20FromHex := func(str string) [20]byte { + var value [20]byte + copy(value[:], bytesFromHex(str)) + return value + } + + bytes32FromHex := func(str string) [32]byte { + var value [32]byte + copy(value[:], bytesFromHex(str)) + return value + } + + requiredConfirmations := uint(6) + + btcChain := newLocalBitcoinChain() + spvChain := newLocalChain() + + walletPublicKeyHash := bytes20FromHex( + "2a621226d6f9916a929c0ab8cc7d3252c1485708", + ) + walletID := bytes32FromHex( + "93fd799256287638b1589bc4c8db1b11fcf873796aabeac9edf4cf238f38e596", + ) + walletP2TR, err := bitcoin.PayToTaproot(walletID) + if err != nil { + t.Fatal(err) + } + + spvChain.setWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{ + WalletID: walletID, + State: tbtc.StateLive, + }, + ) + + redemptionInputTransaction := &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000000, + PublicKeyScript: walletP2TR, + }, + }, + } + redemptionTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: redemptionInputTransaction.Hash(), + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 900000, + PublicKeyScript: bitcoin.Script{0x00, 0x14, 0x01}, + }, + }, + } + + for _, transaction := range []*bitcoin.Transaction{ + redemptionInputTransaction, + redemptionTransaction, + } { + if err := btcChain.BroadcastTransaction(transaction); err != nil { + t.Fatal(err) + } + } + + proof := &bitcoin.SpvProof{ + MerkleProof: []byte{0x01}, + TxIndexInBlock: 2, + BitcoinHeaders: []byte{0x03}, + } + + mockSpvProofAssembler := func( + hash bitcoin.Hash, + confirmations uint, + btcChain bitcoin.Chain, + ) (*bitcoin.Transaction, *bitcoin.SpvProof, error) { + if hash == redemptionTransaction.Hash() && confirmations == requiredConfirmations { + return redemptionTransaction, proof, nil + } + + return nil, nil, fmt.Errorf("error while assembling spv proof") + } + + err = submitRedemptionProof( + redemptionTransaction.Hash(), + requiredConfirmations, + btcChain, + spvChain, + mockSpvProofAssembler, + getGlobalMetricsRecorder(), + ) + if err != nil { + t.Fatal(err) + } + + submittedProofs := spvChain.getSubmittedRedemptionProofs() + testutils.AssertIntsEqual(t, "proofs count", 1, len(submittedProofs)) + + expectedMainUtxo := bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: redemptionInputTransaction.Hash(), + OutputIndex: 0, + }, + Value: 1000000, + } + if diff := deep.Equal(expectedMainUtxo, submittedProofs[0].mainUTXO); diff != nil { + t.Errorf("invalid main UTXO: %v", diff) + } + + testutils.AssertBytesEqual( + t, + walletPublicKeyHash[:], + submittedProofs[0].walletPublicKeyHash[:], + ) +} + func TestGetUnprovenRedemptionTransactions(t *testing.T) { bytesFromHex := func(str string) []byte { value, err := hex.DecodeString(str) @@ -312,3 +446,149 @@ func TestGetUnprovenRedemptionTransactions(t *testing.T) { t.Errorf("invalid unproven transaction hashes: %v", diff) } } + +func TestGetUnprovenRedemptionTransactions_TaprootWallet(t *testing.T) { + bytesFromHex := func(str string) []byte { + value, err := hex.DecodeString(str) + if err != nil { + t.Fatal(err) + } + + return value + } + + bytes20FromHex := func(str string) [20]byte { + var value [20]byte + copy(value[:], bytesFromHex(str)) + return value + } + + bytes32FromHex := func(str string) [32]byte { + var value [32]byte + copy(value[:], bytesFromHex(str)) + return value + } + + historyDepth := uint64(5) + transactionLimit := 10 + + btcChain := newLocalBitcoinChain() + spvChain := newLocalChain() + + currentBlock := uint64(1000) + blockCounter := newMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + spvChain.setBlockCounter(blockCounter) + + walletPublicKeyHash := bytes20FromHex( + "2a621226d6f9916a929c0ab8cc7d3252c1485708", + ) + walletID := bytes32FromHex( + "93fd799256287638b1589bc4c8db1b11fcf873796aabeac9edf4cf238f38e596", + ) + walletP2TR, err := bitcoin.PayToTaproot(walletID) + if err != nil { + t.Fatal(err) + } + redeemerScript, err := bitcoin.PayToWitnessPublicKeyHash( + bytes20FromHex("e3395778bb7f567e5a527ced184320018e59b4de"), + ) + if err != nil { + t.Fatal(err) + } + + mainUtxoTransaction := &bitcoin.Transaction{ + Version: 1, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000000, + PublicKeyScript: walletP2TR, + }, + }, + } + redemptionTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: mainUtxoTransaction.Hash(), + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 10000, + PublicKeyScript: walletP2TR, + }, + { + Value: 900000, + PublicKeyScript: redeemerScript, + }, + }, + } + + for _, transaction := range []*bitcoin.Transaction{ + mainUtxoTransaction, + redemptionTransaction, + } { + if err := btcChain.BroadcastTransaction(transaction); err != nil { + t.Fatal(err) + } + } + + mainUtxo := &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: mainUtxoTransaction.Hash(), + OutputIndex: 0, + }, + Value: 1000000, + } + spvChain.setWallet( + walletPublicKeyHash, + &tbtc.WalletChainData{ + WalletID: walletID, + MainUtxoHash: spvChain.ComputeMainUtxoHash(mainUtxo), + State: tbtc.StateLive, + }, + ) + spvChain.setPendingRedemptionRequest( + walletPublicKeyHash, + &tbtc.RedemptionRequest{ + RedeemerOutputScript: redeemerScript, + }, + ) + + err = spvChain.addPastRedemptionRequestedEvent( + &tbtc.RedemptionRequestedEventFilter{ + StartBlock: currentBlock - historyDepth, + }, + &tbtc.RedemptionRequestedEvent{ + WalletPublicKeyHash: walletPublicKeyHash, + BlockNumber: 100, + }, + ) + if err != nil { + t.Fatal(err) + } + + transactions, err := getUnprovenRedemptionTransactions( + historyDepth, + transactionLimit, + btcChain, + spvChain, + ) + if err != nil { + t.Fatal(err) + } + + testutils.AssertIntsEqual(t, "transactions count", 1, len(transactions)) + if transactions[0].Hash() != redemptionTransaction.Hash() { + t.Errorf( + "invalid transaction hash\nexpected: %v\nactual: %v", + redemptionTransaction.Hash(), + transactions[0].Hash(), + ) + } +} 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/chain.go b/pkg/tbtc/chain.go index 55206f86fb..bcc1e79d72 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. @@ -276,6 +280,14 @@ type BridgeChain interface { filter *DepositRevealedEventFilter, ) ([]*DepositRevealedEvent, error) + // PastTaprootDepositRevealedEvents fetches past Taproot deposit reveal + // events according to the provided filter or unfiltered if the filter is + // nil. Returned events are sorted by the block number in ascending order, + // i.e. the latest event is at the end of the slice. + PastTaprootDepositRevealedEvents( + filter *DepositRevealedEventFilter, + ) ([]*TaprootDepositRevealedEvent, error) + // GetPendingRedemptionRequest gets the on-chain pending redemption request // for the given wallet public key hash and redeemer output script. // The returned bool value indicates whether the request was found or not. @@ -329,6 +341,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 @@ -338,6 +354,7 @@ type NewWalletRegisteredEvent struct { type NewWalletRegisteredEventFilter struct { StartBlock uint64 EndBlock *uint64 + WalletID [][32]byte EcdsaWalletID [][32]byte WalletPublicKeyHash [][20]byte } @@ -388,6 +405,49 @@ func (dre *DepositRevealedEvent) GetWalletPublicKeyHash() [20]byte { return dre.WalletPublicKeyHash } +// TaprootDepositRevealedEvent represents a Taproot deposit reveal event. +// +// The Vault field is nil if the deposit does not target any vault on-chain. +type TaprootDepositRevealedEvent struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + Depositor chain.Address + Amount uint64 + BlindingFactor [8]byte + WalletPublicKeyHash [20]byte + WalletXOnlyPublicKey [32]byte + RefundPublicKeyHash [20]byte + RefundXOnlyPublicKey [32]byte + RefundLocktime [4]byte + Vault *chain.Address + BlockNumber uint64 +} + +func (tdre *TaprootDepositRevealedEvent) unpack(extraData *[32]byte) *Deposit { + return &Deposit{ + Utxo: &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: tdre.FundingTxHash, + OutputIndex: tdre.FundingOutputIndex, + }, + Value: int64(tdre.Amount), + }, + Depositor: tdre.Depositor, + BlindingFactor: tdre.BlindingFactor, + WalletPublicKeyHash: tdre.WalletPublicKeyHash, + WalletXOnlyPublicKey: &tdre.WalletXOnlyPublicKey, + RefundPublicKeyHash: tdre.RefundPublicKeyHash, + RefundXOnlyPublicKey: &tdre.RefundXOnlyPublicKey, + RefundLocktime: tdre.RefundLocktime, + Vault: tdre.Vault, + ExtraData: extraData, + } +} + +func (tdre *TaprootDepositRevealedEvent) GetWalletPublicKeyHash() [20]byte { + return tdre.WalletPublicKeyHash +} + // DepositRevealedEventFilter is a component allowing to filter DepositRevealedEvent. type DepositRevealedEventFilter struct { StartBlock uint64 @@ -413,6 +473,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 @@ -440,6 +504,19 @@ type WalletProposalValidatorChain interface { }, ) error + // ValidateTaprootDepositSweepProposal validates the given Taproot deposit + // sweep proposal against the chain. It requires some additional data about + // the deposits that must be fetched externally. Returns an error if the + // proposal is not valid or nil otherwise. + ValidateTaprootDepositSweepProposal( + walletPublicKeyHash [20]byte, + proposal *DepositSweepProposal, + depositsExtraInfo []struct { + *Deposit + FundingTx *bitcoin.Transaction + }, + ) error + // ValidateRedemptionProposal validates the given redemption proposal // against the chain. Returns an error if the proposal is not valid or // nil otherwise. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index 15bb4c94ca..1cf9f5b6ba 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -47,6 +47,8 @@ type movingFundsParameters = struct { } type localChain struct { + frostWalletRegistryAvailable bool + dkgResultSubmissionHandlersMutex sync.Mutex dkgResultSubmissionHandlers map[int]func(submission *DKGResultSubmittedEvent) @@ -81,6 +83,9 @@ type localChain struct { pastDepositRevealedEventsMutex sync.Mutex pastDepositRevealedEvents map[[32]byte][]*DepositRevealedEvent + pastTaprootDepositRevealedEventsMutex sync.Mutex + pastTaprootDepositRevealedEvents map[[32]byte][]*TaprootDepositRevealedEvent + pastMovingFundsCommitmentSubmittedEventsMutex sync.Mutex pastMovingFundsCommitmentSubmittedEvents map[[32]byte][]*MovingFundsCommitmentSubmittedEvent @@ -733,6 +738,42 @@ func (lc *localChain) setPastDepositRevealedEvents( return nil } +func (lc *localChain) PastTaprootDepositRevealedEvents( + filter *DepositRevealedEventFilter, +) ([]*TaprootDepositRevealedEvent, error) { + lc.pastTaprootDepositRevealedEventsMutex.Lock() + defer lc.pastTaprootDepositRevealedEventsMutex.Unlock() + + eventsKey, err := buildPastDepositRevealedEventsKey(filter) + if err != nil { + return nil, err + } + + events, ok := lc.pastTaprootDepositRevealedEvents[eventsKey] + if !ok { + return []*TaprootDepositRevealedEvent{}, nil + } + + return events, nil +} + +func (lc *localChain) setPastTaprootDepositRevealedEvents( + filter *DepositRevealedEventFilter, + events []*TaprootDepositRevealedEvent, +) error { + lc.pastTaprootDepositRevealedEventsMutex.Lock() + defer lc.pastTaprootDepositRevealedEventsMutex.Unlock() + + eventsKey, err := buildPastDepositRevealedEventsKey(filter) + if err != nil { + return err + } + + lc.pastTaprootDepositRevealedEvents[eventsKey] = events + + return nil +} + func buildPastDepositRevealedEventsKey( filter *DepositRevealedEventFilter, ) ([32]byte, error) { @@ -892,6 +933,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() @@ -906,6 +966,27 @@ func (lc *localChain) IsWalletRegistered(EcdsaWalletID [32]byte) (bool, error) { } } + return false, nil +} + +func (lc *localChain) FrostWalletRegistryAvailable() bool { + return lc.frostWalletRegistryAvailable +} + +func (lc *localChain) IsFrostWalletRegistered(walletID [32]byte) (bool, error) { + lc.walletsMutex.Lock() + defer lc.walletsMutex.Unlock() + + for _, walletData := range lc.wallets { + if walletID == walletData.WalletID { + if walletData.State == StateClosed || + walletData.State == StateTerminated { + return false, nil + } + return true, nil + } + } + return false, fmt.Errorf("wallet not found") } @@ -916,6 +997,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 } @@ -1013,6 +1098,21 @@ func (lc *localChain) ValidateDepositSweepProposal( return nil } +func (lc *localChain) ValidateTaprootDepositSweepProposal( + walletPublicKeyHash [20]byte, + proposal *DepositSweepProposal, + depositsExtraInfo []struct { + *Deposit + FundingTx *bitcoin.Transaction + }, +) error { + return lc.ValidateDepositSweepProposal( + walletPublicKeyHash, + proposal, + depositsExtraInfo, + ) +} + func (lc *localChain) setDepositSweepProposalValidationResult( walletPublicKeyHash [20]byte, proposal *DepositSweepProposal, @@ -1467,6 +1567,7 @@ func ConnectWithKey( blocksByTimestamp: make(map[uint64]uint64), blocksHashesByNumber: make(map[uint64][32]byte), pastDepositRevealedEvents: make(map[[32]byte][]*DepositRevealedEvent), + pastTaprootDepositRevealedEvents: make(map[[32]byte][]*TaprootDepositRevealedEvent), pastMovingFundsCommitmentSubmittedEvents: make(map[[32]byte][]*MovingFundsCommitmentSubmittedEvent), depositSweepProposalValidations: make(map[[32]byte]bool), pendingRedemptionRequests: make(map[[32]byte]*RedemptionRequest), diff --git a/pkg/tbtc/coordination_test.go b/pkg/tbtc/coordination_test.go index 7b869f9671..701886daab 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" @@ -1300,12 +1299,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.go b/pkg/tbtc/deposit.go index 361ed38eb5..7339820595 100644 --- a/pkg/tbtc/deposit.go +++ b/pkg/tbtc/deposit.go @@ -22,6 +22,17 @@ const depositScriptFormat = "14%v7508%v7576a914%v8763ac6776a914%v8804%vb175ac68" // https://github.com/keep-network/tbtc-v2/blob/4b6143974b43297e69a45191f0e2b6a25561e72b/solidity/contracts/bridge/Deposit.sol#L246 const depositWithExtraDataScriptFormat = "14%v7520%v7508%v7576a914%v8763ac6776a914%v8804%vb175ac68" +// taprootDepositRefundScriptFormat is the Taproot-native deposit refund +// tapscript format. The placeholders are: depositor, blindingFactor, +// refundLocktime, and refundXOnlyPublicKey. +const taprootDepositRefundScriptFormat = "14%v7508%v7504%vb17520%vac" + +// taprootDepositWithExtraDataRefundScriptFormat is the Taproot-native deposit +// refund tapscript format with optional 32-byte extra data. The placeholders +// are: depositor, extraData, blindingFactor, refundLocktime, and +// refundXOnlyPublicKey. +const taprootDepositWithExtraDataRefundScriptFormat = "14%v7520%v7508%v7504%vb17520%vac" + // Deposit represents a tBTC deposit. type Deposit struct { // Utxo is the unspent output of the deposit funding transaction that @@ -34,8 +45,14 @@ type Deposit struct { BlindingFactor [8]byte // WalletPublicKeyHash is a 20-byte hash of the target wallet public key. WalletPublicKeyHash [20]byte + // WalletXOnlyPublicKey is the 32-byte Taproot internal wallet key. This + // field is set for Taproot-native deposits only. + WalletXOnlyPublicKey *[32]byte // RefundPublicKeyHash is a 20-byte hash of the refund public key. RefundPublicKeyHash [20]byte + // RefundXOnlyPublicKey is the 32-byte Taproot refund key embedded in the + // refund tapscript. This field is set for Taproot-native deposits only. + RefundXOnlyPublicKey *[32]byte // RefundLocktime is a 4-byte value representing the refund locktime. RefundLocktime [4]byte // Vault is an optional field that holds the host chain address of the @@ -46,9 +63,18 @@ type Deposit struct { ExtraData *[32]byte } +// IsTaproot returns true if this deposit was revealed as Taproot-native. +func (d *Deposit) IsTaproot() bool { + return d.WalletXOnlyPublicKey != nil && d.RefundXOnlyPublicKey != nil +} + // Script constructs the deposit P2(W)SH Bitcoin script. This function // assumes the deposit's fields are correctly set. func (d *Deposit) Script() ([]byte, error) { + if d.IsTaproot() { + return nil, fmt.Errorf("Taproot deposit does not have a P2(W)SH script") + } + depositorBytes, err := hex.DecodeString( strings.TrimPrefix(d.Depositor.String(), "0x"), ) @@ -84,3 +110,54 @@ func (d *Deposit) Script() ([]byte, error) { return hex.DecodeString(script) } + +// TaprootRefundScript constructs the deposit refund tapscript. This function +// assumes the deposit's Taproot fields are correctly set. +func (d *Deposit) TaprootRefundScript() ([]byte, error) { + if !d.IsTaproot() { + return nil, fmt.Errorf("deposit is not Taproot-native") + } + + depositorBytes, err := hex.DecodeString( + strings.TrimPrefix(d.Depositor.String(), "0x"), + ) + if err != nil { + return nil, fmt.Errorf("cannot decode depositor field: [%v]", err) + } + if len(depositorBytes) != 20 { + return nil, fmt.Errorf("wrong byte length of depositor field") + } + + var script string + + if d.ExtraData != nil { + script = fmt.Sprintf( + taprootDepositWithExtraDataRefundScriptFormat, + hex.EncodeToString(depositorBytes), + hex.EncodeToString(d.ExtraData[:]), + hex.EncodeToString(d.BlindingFactor[:]), + hex.EncodeToString(d.RefundLocktime[:]), + hex.EncodeToString(d.RefundXOnlyPublicKey[:]), + ) + } else { + script = fmt.Sprintf( + taprootDepositRefundScriptFormat, + hex.EncodeToString(depositorBytes), + hex.EncodeToString(d.BlindingFactor[:]), + hex.EncodeToString(d.RefundLocktime[:]), + hex.EncodeToString(d.RefundXOnlyPublicKey[:]), + ) + } + + return hex.DecodeString(script) +} + +// TaprootMerkleRoot returns the Taproot script tree root for this deposit. +func (d *Deposit) TaprootMerkleRoot() ([32]byte, error) { + refundScript, err := d.TaprootRefundScript() + if err != nil { + return [32]byte{}, err + } + + return bitcoin.TaprootLeafHash(refundScript) +} diff --git a/pkg/tbtc/deposit_sweep.go b/pkg/tbtc/deposit_sweep.go index 824ce29d28..3fd95aa032 100644 --- a/pkg/tbtc/deposit_sweep.go +++ b/pkg/tbtc/deposit_sweep.go @@ -159,8 +159,8 @@ func (dsa *depositSweepAction) execute() error { return fmt.Errorf("validate proposal step failed: [%v]", err) } - walletMainUtxo, err := DetermineWalletMainUtxo( - walletPublicKeyHash, + walletMainUtxo, err := DetermineWalletMainUtxoForPublicKey( + dsa.wallet().publicKey, dsa.chain, dsa.btcChain, ) @@ -175,8 +175,8 @@ func (dsa *depositSweepAction) execute() error { ) } - err = EnsureWalletSyncedBetweenChains( - walletPublicKeyHash, + err = EnsureWalletSyncedBetweenChainsForPublicKey( + dsa.wallet().publicKey, walletMainUtxo, dsa.chain, dsa.btcChain, @@ -288,6 +288,14 @@ func ValidateDepositSweepProposal( filter *DepositRevealedEventFilter, ) ([]*DepositRevealedEvent, error) + // PastTaprootDepositRevealedEvents fetches past Taproot deposit reveal + // events according to the provided filter or unfiltered if the filter + // is nil. Returned events are sorted by the block number in the + // ascending order, i.e. the latest event is at the end of the slice. + PastTaprootDepositRevealedEvents( + filter *DepositRevealedEventFilter, + ) ([]*TaprootDepositRevealedEvent, error) + // ValidateDepositSweepProposal validates the given deposit sweep proposal // against the chain. It requires some additional data about the deposits // that must be fetched externally. Returns an error if the proposal is @@ -301,6 +309,19 @@ func ValidateDepositSweepProposal( }, ) error + // ValidateTaprootDepositSweepProposal validates the given Taproot + // deposit sweep proposal against the chain. It requires some additional + // data about the deposits that must be fetched externally. Returns an + // error if the proposal is not valid or nil otherwise. + ValidateTaprootDepositSweepProposal( + walletPublicKeyHash [20]byte, + proposal *DepositSweepProposal, + depositsExtraInfo []struct { + *Deposit + FundingTx *bitcoin.Transaction + }, + ) error + // GetDepositRequest gets the on-chain deposit request for the given // funding transaction hash and output index.The returned values represent: // - deposit request which is non-nil only when the deposit request was @@ -331,6 +352,8 @@ func ValidateDepositSweepProposal( return nil, fmt.Errorf("proposal's reveal blocks list has a wrong length") } + taprootDepositsCount := 0 + for i, depositKey := range proposal.DepositsKeys { depositDisplayIndex := fmt.Sprintf("%v/%v", i+1, len(proposal.DepositsKeys)) @@ -377,6 +400,12 @@ func ValidateDepositSweepProposal( revealBlock := proposal.DepositsRevealBlocks[i].Uint64() + filter := &DepositRevealedEventFilter{ + StartBlock: revealBlock, + EndBlock: &revealBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + } + // We need to fetch the past DepositRevealed event for the given deposit. // It may be tempting to fetch such events for all deposit keys // in the proposal using a single call, however, this solution has @@ -387,11 +416,7 @@ func ValidateDepositSweepProposal( // We have the revealBlock passed by the coordinator within the proposal // so, we can use it to make a narrow call. Moreover, we use the // wallet PKH as additional filter to limit the size of returned data. - events, err := chain.PastDepositRevealedEvents(&DepositRevealedEventFilter{ - StartBlock: revealBlock, - EndBlock: &revealBlock, - WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, - }) + events, err := chain.PastDepositRevealedEvents(filter) if err != nil { return nil, fmt.Errorf( "cannot get on-chain DepositRevealed events for deposit [%v]: [%v]", @@ -411,9 +436,27 @@ func ValidateDepositSweepProposal( } } - if matchingEvent == nil { + taprootEvents, err := chain.PastTaprootDepositRevealedEvents(filter) + if err != nil { return nil, fmt.Errorf( - "no matching DepositRevealed event for deposit [%v]: [%v]", + "cannot get on-chain TaprootDepositRevealed events for deposit [%v]: [%v]", + depositDisplayIndex, + err, + ) + } + + var matchingTaprootEvent *TaprootDepositRevealedEvent + for _, event := range taprootEvents { + if event.FundingTxHash == depositKey.FundingTxHash && + event.FundingOutputIndex == depositKey.FundingOutputIndex { + matchingTaprootEvent = event + break + } + } + + if matchingEvent == nil && matchingTaprootEvent == nil { + return nil, fmt.Errorf( + "no matching DepositRevealed or TaprootDepositRevealed event for deposit [%v]: [%v]", depositDisplayIndex, err, ) @@ -441,18 +484,40 @@ func ValidateDepositSweepProposal( *Deposit FundingTx *bitcoin.Transaction }{ - Deposit: matchingEvent.unpack(depositRequest.ExtraData), + Deposit: func() *Deposit { + if matchingTaprootEvent != nil { + taprootDepositsCount++ + return matchingTaprootEvent.unpack(depositRequest.ExtraData) + } + + return matchingEvent.unpack(depositRequest.ExtraData) + }(), FundingTx: fundingTx, } } + if taprootDepositsCount > 0 && taprootDepositsCount != len(proposal.DepositsKeys) { + return nil, fmt.Errorf( + "mixed legacy and Taproot deposits are not supported in one sweep proposal", + ) + } + validateProposalLogger.Infof("calling chain for proposal validation") - err := chain.ValidateDepositSweepProposal( - walletPublicKeyHash, - proposal, - depositExtraInfo, - ) + var err error + if taprootDepositsCount > 0 { + err = chain.ValidateTaprootDepositSweepProposal( + walletPublicKeyHash, + proposal, + depositExtraInfo, + ) + } else { + err = chain.ValidateDepositSweepProposal( + walletPublicKeyHash, + proposal, + depositExtraInfo, + ) + } if err != nil { return nil, fmt.Errorf("deposit sweep proposal is invalid: [%v]", err) } @@ -508,6 +573,41 @@ func assembleDepositSweepTransaction( return nil, fmt.Errorf("at least one deposit is required") } + taprootDepositsCount := 0 + for _, deposit := range deposits { + if deposit.IsTaproot() { + taprootDepositsCount++ + } + } + + if taprootDepositsCount > 0 && taprootDepositsCount != len(deposits) { + return nil, fmt.Errorf( + "mixed legacy and Taproot deposits are not supported in one sweep transaction", + ) + } + + taprootSweep := taprootDepositsCount > 0 + + if !taprootSweep && walletMainUtxo != nil { + scriptType, err := walletMainUtxoScriptType( + bitcoinChain, + walletMainUtxo, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot inspect wallet main UTXO script: [%v]", + err, + ) + } + + if scriptType == bitcoin.P2TRScript { + return nil, fmt.Errorf( + "legacy deposit sweeps are not supported for " + + "Taproot wallet main UTXOs", + ) + } + } + builder := bitcoin.NewTransactionBuilder(bitcoinChain) if walletMainUtxo != nil { @@ -521,29 +621,73 @@ func assembleDepositSweepTransaction( } for i, deposit := range deposits { - depositScript, err := deposit.Script() - if err != nil { - return nil, fmt.Errorf( - "cannot get script for deposit [%v]: [%v]", - i, - err, + if deposit.IsTaproot() { + merkleRoot, err := deposit.TaprootMerkleRoot() + if err != nil { + return nil, fmt.Errorf( + "cannot compute Taproot merkle root for deposit [%v]: [%v]", + i, + err, + ) + } + + err = builder.AddTaprootKeyPathInputWithMerkleRoot( + deposit.Utxo, + *deposit.WalletXOnlyPublicKey, + merkleRoot, ) + if err != nil { + return nil, fmt.Errorf( + "cannot add input pointing to Taproot deposit [%v] UTXO: [%v]", + i, + err, + ) + } + } else { + depositScript, err := deposit.Script() + if err != nil { + return nil, fmt.Errorf( + "cannot get script for deposit [%v]: [%v]", + i, + err, + ) + } + + err = builder.AddScriptHashInput(deposit.Utxo, depositScript) + if err != nil { + return nil, fmt.Errorf( + "cannot add input pointing to deposit [%v] UTXO: [%v]", + i, + err, + ) + } } + } - err = builder.AddScriptHashInput(deposit.Utxo, depositScript) + if taprootSweep && !builder.HasOnlyTaprootKeyPathInputs() { + return nil, fmt.Errorf( + "Taproot deposit sweep requires a Taproot wallet main UTXO", + ) + } + + var outputScript bitcoin.Script + var err error + if taprootSweep { + walletXOnlyPublicKey, err := walletXOnlyPublicKey(walletPublicKey) if err != nil { - return nil, fmt.Errorf( - "cannot add input pointing to deposit [%v] UTXO: [%v]", - i, - err, - ) + return nil, err } - } - walletPublicKeyHash := bitcoin.PublicKeyHash(walletPublicKey) - outputScript, err := bitcoin.PayToWitnessPublicKeyHash(walletPublicKeyHash) - if err != nil { - return nil, fmt.Errorf("cannot compute output script: [%v]", err) + outputScript, err = bitcoin.PayToTaproot(walletXOnlyPublicKey) + if err != nil { + return nil, fmt.Errorf("cannot compute Taproot output script: [%v]", err) + } + } else { + walletPublicKeyHash := bitcoin.PublicKeyHash(walletPublicKey) + outputScript, err = bitcoin.PayToWitnessPublicKeyHash(walletPublicKeyHash) + if err != nil { + return nil, fmt.Errorf("cannot compute output script: [%v]", err) + } } outputValue := builder.TotalInputsValue() - fee diff --git a/pkg/tbtc/deposit_sweep_test.go b/pkg/tbtc/deposit_sweep_test.go index c98f75a3c0..ac03321cbf 100644 --- a/pkg/tbtc/deposit_sweep_test.go +++ b/pkg/tbtc/deposit_sweep_test.go @@ -2,15 +2,19 @@ package tbtc import ( "context" + "crypto/ecdsa" + "encoding/hex" "fmt" "math/big" + "strings" "testing" "time" - "github.com/keep-network/keep-core/pkg/tecdsa" - + "github.com/btcsuite/btcd/btcec" "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/tbtc/internal/test" ) @@ -78,7 +82,16 @@ func TestDepositSweepAction_Execute(t *testing.T) { *Deposit FundingTx *bitcoin.Transaction }{ - Deposit: (*Deposit)(deposit), + Deposit: &Deposit{ + Utxo: deposit.Utxo, + Depositor: deposit.Depositor, + BlindingFactor: deposit.BlindingFactor, + WalletPublicKeyHash: deposit.WalletPublicKeyHash, + RefundPublicKeyHash: deposit.RefundPublicKeyHash, + RefundLocktime: deposit.RefundLocktime, + Vault: deposit.Vault, + ExtraData: deposit.ExtraData, + }, FundingTx: fundingTx, } @@ -171,16 +184,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 @@ -318,3 +330,415 @@ func TestAssembleDepositSweepTransaction(t *testing.T) { }) } } + +func TestAssembleDepositSweepTransaction_TaprootDeposit(t *testing.T) { + hexToSlice := func(hexString string) []byte { + bytes, err := hex.DecodeString(hexString) + if err != nil { + t.Fatalf("error while converting [%v]: [%v]", hexString, err) + } + return bytes + } + + var walletXOnlyPublicKey [32]byte + copy( + walletXOnlyPublicKey[:], + hexToSlice("2336f65004d8f122f1fe947ebd009a8b4add3a0d937356d568e30f7fcc2e4008"), + ) + + compressedWalletPublicKey := append([]byte{0x02}, walletXOnlyPublicKey[:]...) + parsedWalletPublicKey, err := btcec.ParsePubKey( + compressedWalletPublicKey, + btcec.S256(), + ) + if err != nil { + t.Fatal(err) + } + walletPublicKey := &ecdsa.PublicKey{ + Curve: btcec.S256(), + X: parsedWalletPublicKey.X, + Y: parsedWalletPublicKey.Y, + } + + var refundXOnlyPublicKey [32]byte + copy( + refundXOnlyPublicKey[:], + hexToSlice("11223344556677889900aabbccddeeff00112233445566778899aabbccddeeff"), + ) + + depositOne := &Deposit{ + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + WalletXOnlyPublicKey: &walletXOnlyPublicKey, + RefundXOnlyPublicKey: &refundXOnlyPublicKey, + } + copy(depositOne.BlindingFactor[:], hexToSlice("f9f0c90d00039523")) + copy(depositOne.WalletPublicKeyHash[:], hexToSlice("c92a772f11bc97d8938a16a9db435401f4e6a7bc")) + copy(depositOne.RefundPublicKeyHash[:], hexToSlice("c2a27a88d8d03e271e8edc556923e9398619f17c")) + copy(depositOne.RefundLocktime[:], hexToSlice("60bcea61")) + + merkleRootOne, err := depositOne.TaprootMerkleRoot() + if err != nil { + t.Fatal(err) + } + + fundingOutputScriptOne, err := bitcoin.PayToTaprootWithScriptTree( + walletXOnlyPublicKey, + merkleRootOne, + ) + if err != nil { + t.Fatal(err) + } + + depositTwo := &Deposit{ + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + WalletXOnlyPublicKey: &walletXOnlyPublicKey, + RefundXOnlyPublicKey: &refundXOnlyPublicKey, + } + copy(depositTwo.BlindingFactor[:], hexToSlice("f9f0c90d00039523")) + copy(depositTwo.WalletPublicKeyHash[:], hexToSlice("c92a772f11bc97d8938a16a9db435401f4e6a7bc")) + copy(depositTwo.RefundPublicKeyHash[:], hexToSlice("c2a27a88d8d03e271e8edc556923e9398619f17c")) + copy(depositTwo.RefundLocktime[:], hexToSlice("60bcea61")) + var extraData [32]byte + copy( + extraData[:], + hexToSlice( + "a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55196396b03d9bda", + ), + ) + depositTwo.ExtraData = &extraData + + merkleRootTwo, err := depositTwo.TaprootMerkleRoot() + if err != nil { + t.Fatal(err) + } + + fundingOutputScriptTwo, err := bitcoin.PayToTaprootWithScriptTree( + walletXOnlyPublicKey, + merkleRootTwo, + ) + if err != nil { + t.Fatal(err) + } + + var previousTxHash bitcoin.Hash + copy(previousTxHash[:], hexToSlice("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")) + fundingTx := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: previousTxHash, + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 100000, + PublicKeyScript: fundingOutputScriptOne, + }, + { + Value: 110000, + PublicKeyScript: fundingOutputScriptTwo, + }, + }, + } + + bitcoinChain := newLocalBitcoinChain() + if err := bitcoinChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatal(err) + } + + depositOne.Utxo = &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTx.Hash(), + OutputIndex: 0, + }, + Value: 100000, + } + depositTwo.Utxo = &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTx.Hash(), + OutputIndex: 1, + }, + Value: 110000, + } + + builder, err := assembleDepositSweepTransaction( + bitcoinChain, + walletPublicKey, + nil, + []*Deposit{depositOne, depositTwo}, + 1000, + ) + if err != nil { + t.Fatal(err) + } + + if !builder.HasOnlyTaprootKeyPathInputs() { + t.Fatal("expected only Taproot key-path inputs") + } + + merkleRoots := builder.TaprootKeyPathInputMerkleRoots() + if len(merkleRoots) != 2 || merkleRoots[0] == nil || merkleRoots[1] == nil { + t.Fatalf("expected two Taproot merkle roots") + } + testutils.AssertBytesEqual(t, merkleRootOne[:], merkleRoots[0][:]) + testutils.AssertBytesEqual(t, merkleRootTwo[:], merkleRoots[1][:]) + + unsignedTx := builder.UnsignedTransaction() + if len(unsignedTx.Outputs) != 1 { + t.Fatalf("unexpected outputs count: [%v]", len(unsignedTx.Outputs)) + } + + expectedWalletOutputScript, err := bitcoin.PayToTaproot(walletXOnlyPublicKey) + if err != nil { + t.Fatal(err) + } + testutils.AssertBytesEqual( + t, + expectedWalletOutputScript, + unsignedTx.Outputs[0].PublicKeyScript, + ) + testutils.AssertIntsEqual( + t, + "output value", + 209000, + int(unsignedTx.Outputs[0].Value), + ) +} + +func TestAssembleDepositSweepTransaction_RejectsLegacyDepositsWithTaprootWalletMainUtxo( + t *testing.T, +) { + bitcoinChain := newLocalBitcoinChain() + walletPublicKey := testWalletPublicKeyFromXOnly( + t, + "2336f65004d8f122f1fe947ebd009a8b4add3a0d937356d568e30f7fcc2e4008", + ) + walletMainUtxo := testTaprootWalletMainUtxo( + t, + bitcoinChain, + walletPublicKey, + ) + + _, err := assembleDepositSweepTransaction( + bitcoinChain, + walletPublicKey, + walletMainUtxo, + []*Deposit{ + { + Depositor: chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637"), + }, + }, + 1000, + ) + if err == nil { + t.Fatal("expected legacy deposit sweep with Taproot main UTXO rejection") + } + if !strings.Contains(err.Error(), "legacy deposit sweeps") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +func TestValidateDepositSweepProposal_PrefersTaprootRevealOverCompatibilityReveal(t *testing.T) { + hexToSlice := func(hexString string) []byte { + bytes, err := hex.DecodeString(hexString) + if err != nil { + t.Fatalf("error while converting [%v]: [%v]", hexString, err) + } + return bytes + } + + var fundingTxHash bitcoin.Hash + copy(fundingTxHash[:], hexToSlice("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")) + + fundingTx := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTxHash, + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 100000, + PublicKeyScript: bitcoin.Script{ + 0x51, 0x20, + 0x23, 0x36, 0xf6, 0x50, 0x04, 0xd8, 0xf1, 0x22, + 0xf1, 0xfe, 0x94, 0x7e, 0xbd, 0x00, 0x9a, 0x8b, + 0x4a, 0xdd, 0x3a, 0x0d, 0x93, 0x73, 0x56, 0xd5, + 0x68, 0xe3, 0x0f, 0x7f, 0xcc, 0x2e, 0x40, 0x08, + }, + }, + }, + } + + bitcoinChain := newLocalBitcoinChain() + if err := bitcoinChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatal(err) + } + + fundingOutputIndex := uint32(0) + revealBlock := uint64(123) + var blindingFactor [8]byte + copy(blindingFactor[:], hexToSlice("f9f0c90d00039523")) + var walletPublicKeyHash [20]byte + copy(walletPublicKeyHash[:], hexToSlice("c92a772f11bc97d8938a16a9db435401f4e6a7bc")) + var walletXOnlyPublicKey [32]byte + copy( + walletXOnlyPublicKey[:], + hexToSlice("2336f65004d8f122f1fe947ebd009a8b4add3a0d937356d568e30f7fcc2e4008"), + ) + var refundPublicKeyHash [20]byte + copy(refundPublicKeyHash[:], hexToSlice("c2a27a88d8d03e271e8edc556923e9398619f17c")) + var refundXOnlyPublicKey [32]byte + copy( + refundXOnlyPublicKey[:], + hexToSlice("11223344556677889900aabbccddeeff00112233445566778899aabbccddeeff"), + ) + var refundLocktime [4]byte + copy(refundLocktime[:], hexToSlice("60bcea61")) + depositor := chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637") + + proposal := &DepositSweepProposal{ + DepositsKeys: []struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + }{ + { + FundingTxHash: fundingTx.Hash(), + FundingOutputIndex: fundingOutputIndex, + }, + }, + SweepTxFee: big.NewInt(1000), + DepositsRevealBlocks: []*big.Int{ + big.NewInt(int64(revealBlock)), + }, + } + + validationChain := &depositSweepValidationChainStub{ + legacyEvents: []*DepositRevealedEvent{ + { + FundingTxHash: fundingTx.Hash(), + FundingOutputIndex: fundingOutputIndex, + Depositor: depositor, + Amount: 100000, + BlindingFactor: blindingFactor, + WalletPublicKeyHash: walletPublicKeyHash, + RefundPublicKeyHash: refundPublicKeyHash, + RefundLocktime: refundLocktime, + BlockNumber: revealBlock, + }, + }, + taprootEvents: []*TaprootDepositRevealedEvent{ + { + FundingTxHash: fundingTx.Hash(), + FundingOutputIndex: fundingOutputIndex, + Depositor: depositor, + Amount: 100000, + BlindingFactor: blindingFactor, + WalletPublicKeyHash: walletPublicKeyHash, + WalletXOnlyPublicKey: walletXOnlyPublicKey, + RefundPublicKeyHash: refundPublicKeyHash, + RefundXOnlyPublicKey: refundXOnlyPublicKey, + RefundLocktime: refundLocktime, + BlockNumber: revealBlock, + }, + }, + depositRequest: &DepositChainRequest{ + Depositor: depositor, + Amount: 100000, + }, + } + + deposits, err := ValidateDepositSweepProposal( + logger.With(), + walletPublicKeyHash, + proposal, + 1, + validationChain, + bitcoinChain, + ) + if err != nil { + t.Fatal(err) + } + + if validationChain.legacyValidationCalled { + t.Fatal("legacy validation should not be called when a Taproot event matches") + } + if !validationChain.taprootValidationCalled { + t.Fatal("Taproot validation was not called") + } + if len(deposits) != 1 { + t.Fatalf("unexpected deposits count: [%v]", len(deposits)) + } + if !deposits[0].IsTaproot() { + t.Fatal("expected validated deposit to be Taproot-native") + } +} + +type depositSweepValidationChainStub struct { + legacyEvents []*DepositRevealedEvent + taprootEvents []*TaprootDepositRevealedEvent + depositRequest *DepositChainRequest + + legacyValidationCalled bool + taprootValidationCalled bool +} + +func (dsvcs *depositSweepValidationChainStub) PastDepositRevealedEvents( + filter *DepositRevealedEventFilter, +) ([]*DepositRevealedEvent, error) { + return dsvcs.legacyEvents, nil +} + +func (dsvcs *depositSweepValidationChainStub) PastTaprootDepositRevealedEvents( + filter *DepositRevealedEventFilter, +) ([]*TaprootDepositRevealedEvent, error) { + return dsvcs.taprootEvents, nil +} + +func (dsvcs *depositSweepValidationChainStub) ValidateDepositSweepProposal( + walletPublicKeyHash [20]byte, + proposal *DepositSweepProposal, + depositsExtraInfo []struct { + *Deposit + FundingTx *bitcoin.Transaction + }, +) error { + dsvcs.legacyValidationCalled = true + return fmt.Errorf("legacy validation should not be called") +} + +func (dsvcs *depositSweepValidationChainStub) ValidateTaprootDepositSweepProposal( + walletPublicKeyHash [20]byte, + proposal *DepositSweepProposal, + depositsExtraInfo []struct { + *Deposit + FundingTx *bitcoin.Transaction + }, +) error { + dsvcs.taprootValidationCalled = true + + if len(depositsExtraInfo) != 1 { + return fmt.Errorf("unexpected deposits extra info count: [%v]", len(depositsExtraInfo)) + } + if !depositsExtraInfo[0].Deposit.IsTaproot() { + return fmt.Errorf("expected Taproot deposit extra info") + } + + return nil +} + +func (dsvcs *depositSweepValidationChainStub) GetDepositRequest( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) (*DepositChainRequest, bool, error) { + return dsvcs.depositRequest, true, nil +} diff --git a/pkg/tbtc/deposit_test.go b/pkg/tbtc/deposit_test.go index 72b86344cd..ef978837d7 100644 --- a/pkg/tbtc/deposit_test.go +++ b/pkg/tbtc/deposit_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "testing" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/chain" "github.com/keep-network/keep-core/internal/testutils" @@ -82,3 +83,113 @@ func TestDeposit_Script(t *testing.T) { }) } } + +func TestDeposit_TaprootRefundScript(t *testing.T) { + hexToSlice := func(hexString string) []byte { + bytes, err := hex.DecodeString(hexString) + if err != nil { + t.Fatalf("error while converting [%v]: [%v]", hexString, err) + } + return bytes + } + + var tests = map[string]struct { + extraData string + expectedScript string + expectedMerkleRoot string + expectedTaprootKey string + expectedOutputScript string + }{ + "no extra data": { + extraData: "", + expectedScript: "14934b98637ca318a4d6e7ca6ffd1690b8e77df6377508" + + "f9f0c90d00039523750460bcea61b1752011223344556677889900aabb" + + "ccddeeff00112233445566778899aabbccddeeffac", + expectedMerkleRoot: "3d6f9a2fea1de0a6c260d1fbc0343c9b2ed84307e6a7" + + "231139b78438448ee8c0", + expectedTaprootKey: "90e7ce2b6cd476b7a1c2c7f6585c3fd0eae4379a508e" + + "981ed422b3e28b9ae8c2", + expectedOutputScript: "512090e7ce2b6cd476b7a1c2c7f6585c3fd0eae4379" + + "a508e981ed422b3e28b9ae8c2", + }, + "with extra data": { + extraData: "a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55" + + "196396b03d9bda", + expectedScript: "14934b98637ca318a4d6e7ca6ffd1690b8e77df6377520" + + "a9b38ea6435c8941d6eda6a46b68e3e2117196995bd154ab55196396" + + "b03d9bda7508f9f0c90d00039523750460bcea61b175201122334455" + + "6677889900aabbccddeeff00112233445566778899aabbccddeeffac", + expectedMerkleRoot: "6968648895261db4f667ff977b3bbd9b4684fe756050" + + "894b092fd0e24e24f90f", + expectedTaprootKey: "b57ad22351a7a074b6588836d08fbecae35b61ef9eeb" + + "35376a1c5f3d6049376e", + expectedOutputScript: "5120b57ad22351a7a074b6588836d08fbecae35b61ef" + + "9eeb35376a1c5f3d6049376e", + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + d := new(Deposit) + d.Depositor = chain.Address("934b98637ca318a4d6e7ca6ffd1690b8e77df637") + copy(d.BlindingFactor[:], hexToSlice("f9f0c90d00039523")) + copy(d.WalletPublicKeyHash[:], hexToSlice("c92a772f11bc97d8938a16a9db435401f4e6a7bc")) + copy(d.RefundPublicKeyHash[:], hexToSlice("c2a27a88d8d03e271e8edc556923e9398619f17c")) + copy(d.RefundLocktime[:], hexToSlice("60bcea61")) + + var walletXOnlyPublicKey [32]byte + copy( + walletXOnlyPublicKey[:], + hexToSlice("2336f65004d8f122f1fe947ebd009a8b4add3a0d937356d568e30f7fcc2e4008"), + ) + d.WalletXOnlyPublicKey = &walletXOnlyPublicKey + + var refundXOnlyPublicKey [32]byte + copy( + refundXOnlyPublicKey[:], + hexToSlice("11223344556677889900aabbccddeeff00112233445566778899aabbccddeeff"), + ) + d.RefundXOnlyPublicKey = &refundXOnlyPublicKey + + if len(test.extraData) > 0 { + var extraData [32]byte + copy(extraData[:], hexToSlice(test.extraData)) + d.ExtraData = &extraData + } + + refundScript, err := d.TaprootRefundScript() + if err != nil { + t.Fatal(err) + } + testutils.AssertBytesEqual(t, hexToSlice(test.expectedScript), refundScript) + + merkleRoot, err := d.TaprootMerkleRoot() + if err != nil { + t.Fatal(err) + } + testutils.AssertBytesEqual(t, hexToSlice(test.expectedMerkleRoot), merkleRoot[:]) + + outputScript, err := bitcoin.PayToTaprootWithScriptTree( + *d.WalletXOnlyPublicKey, + merkleRoot, + ) + if err != nil { + t.Fatal(err) + } + testutils.AssertBytesEqual( + t, + hexToSlice(test.expectedOutputScript), + outputScript, + ) + + outputKey, err := bitcoin.TaprootOutputKey( + *d.WalletXOnlyPublicKey, + &merkleRoot, + ) + if err != nil { + t.Fatal(err) + } + testutils.AssertBytesEqual(t, hexToSlice(test.expectedTaprootKey), outputKey[:]) + }) + } +} diff --git a/pkg/tbtc/dkg.go b/pkg/tbtc/dkg.go index 177e225a18..51b9723cc3 100644 --- a/pkg/tbtc/dkg.go +++ b/pkg/tbtc/dkg.go @@ -91,16 +91,24 @@ func newDkgExecutor( scheduler *generator.Scheduler, waitForBlockFn waitForBlockFn, ) *dkgExecutor { - tecdsaExecutor := dkg.NewExecutor( - logger, - scheduler, - workPersistence, - config.PreParamsPoolSize, - config.PreParamsGenerationTimeout, - config.PreParamsGenerationDelay, - config.PreParamsGenerationConcurrency, - config.KeyGenerationConcurrency, - ) + var tecdsaExecutor *dkg.Executor + if config.PreParamsPoolSize > 0 { + tecdsaExecutor = dkg.NewExecutor( + logger, + scheduler, + workPersistence, + config.PreParamsPoolSize, + config.PreParamsGenerationTimeout, + config.PreParamsGenerationDelay, + config.PreParamsGenerationConcurrency, + config.KeyGenerationConcurrency, + ) + } else { + logger.Info( + "ECDSA DKG pre-parameters pool is disabled; " + + "ECDSA DKG execution will be skipped", + ) + } return &dkgExecutor{ groupParameters: groupParameters, @@ -126,6 +134,10 @@ func (de *dkgExecutor) setMetricsRecorder(recorder interface { // preParamsCount returns the current count of the ECDSA DKG pre-parameters. func (de *dkgExecutor) preParamsCount() int { + if de.tecdsaExecutor == nil { + return 0 + } + return de.tecdsaExecutor.PreParamsCount() } @@ -145,6 +157,11 @@ func (de *dkgExecutor) executeDkgIfEligible( zap.String("seed", fmt.Sprintf("0x%x", seed)), ) + if de.tecdsaExecutor == nil { + dkgLogger.Info("ECDSA DKG execution is disabled") + return + } + dkgLogger.Info("checking eligibility for DKG") memberIndexes, groupSelectionResult, err := de.checkEligibility( dkgLogger, @@ -521,11 +538,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/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/dkg_test.go b/pkg/tbtc/dkg_test.go index 547d1b5079..54d712dbdb 100644 --- a/pkg/tbtc/dkg_test.go +++ b/pkg/tbtc/dkg_test.go @@ -3,6 +3,7 @@ package tbtc import ( "context" "fmt" + "math/big" "reflect" "testing" "time" @@ -13,12 +14,44 @@ import ( "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/generator" "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" "github.com/keep-network/keep-core/pkg/tecdsa/dkg" ) +func TestDkgExecutor_DisablesECDSAPreParamsWhenPoolSizeZero(t *testing.T) { + executor := newDkgExecutor( + &GroupParameters{}, + nil, + "", + nil, + nil, + nil, + nil, + Config{PreParamsPoolSize: 0}, + nil, + &generator.Scheduler{}, + nil, + ) + + if executor.tecdsaExecutor != nil { + t.Fatal("expected ECDSA DKG executor to be disabled") + } + + testutils.AssertIntsEqual( + t, + "ECDSA pre-parameters count", + 0, + executor.preParamsCount(), + ) + + // An explicit zero pre-parameters pool disables the legacy ECDSA DKG path. + // This should be a no-op and must not require chain/network dependencies. + executor.executeDkgIfEligible(big.NewInt(1), 0, 0) +} + func TestDkgExecutor_RegisterSigner(t *testing.T) { testData, err := tecdsatest.LoadPrivateKeyShareTestFixtures(1) if err != nil { diff --git a/pkg/tbtc/frost_dkg_chain.go b/pkg/tbtc/frost_dkg_chain.go new file mode 100644 index 0000000000..94a168fb9f --- /dev/null +++ b/pkg/tbtc/frost_dkg_chain.go @@ -0,0 +1,103 @@ +package tbtc + +import ( + "math/big" + + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost/registry" + "github.com/keep-network/keep-core/pkg/subscription" +) + +// FrostDKGChain defines the FROST wallet-registry chain surface. It is kept +// separate from the legacy ECDSA DKG chain so the existing coordinator remains +// unchanged until FROST creation is explicitly enabled. +type FrostDKGChain interface { + FrostWalletRegistryAvailable() bool + + OnBridgeNewWalletRequested( + func(event *BridgeNewWalletRequestedEvent), + ) subscription.EventSubscription + + OnFrostDKGStarted( + func(event *FrostDKGStartedEvent), + ) subscription.EventSubscription + PastFrostDKGStartedEvents( + filter *FrostDKGStartedEventFilter, + ) ([]*FrostDKGStartedEvent, error) + + OnFrostDKGResultSubmitted( + func(event *FrostDKGResultSubmittedEvent), + ) subscription.EventSubscription + PastFrostDKGResultSubmittedEvents( + filter *FrostDKGResultSubmittedEventFilter, + ) ([]*FrostDKGResultSubmittedEvent, error) + OnFrostDKGResultChallenged( + func(event *FrostDKGResultChallengedEvent), + ) subscription.EventSubscription + OnFrostDKGResultApproved( + func(event *FrostDKGResultApprovedEvent), + ) subscription.EventSubscription + + SelectFrostGroup() (*GroupSelectionResult, error) + GetFrostDKGState() (DKGState, error) + IsFrostDKGResultValid(result *registry.Result) (bool, string, error) + CalculateFrostDKGResultDigest( + seed *big.Int, + result *registry.Result, + ) ([32]byte, error) + SubmitFrostDKGResult(result *registry.Result) error + ChallengeFrostDKGResult(result *registry.Result) error + ApproveFrostDKGResult(result *registry.Result) error + FrostDKGParameters() (*DKGParameters, error) +} + +// BridgeNewWalletRequestedEvent represents Bridge.NewWalletRequested. +type BridgeNewWalletRequestedEvent struct { + BlockNumber uint64 +} + +// FrostDKGStartedEvent represents the FrostWalletRegistry.DkgStarted event. +type FrostDKGStartedEvent struct { + Seed *big.Int + BlockNumber uint64 +} + +// FrostDKGStartedEventFilter is a component allowing to filter FROST +// DkgStarted events. +type FrostDKGStartedEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + Seed []*big.Int +} + +// FrostDKGResultSubmittedEvent represents a FROST DKG result submission. +type FrostDKGResultSubmittedEvent struct { + Seed *big.Int + ResultHash DKGChainResultHash + Result *registry.Result + BlockNumber uint64 +} + +// FrostDKGResultSubmittedEventFilter is a component allowing to filter FROST +// DkgResultSubmitted events. +type FrostDKGResultSubmittedEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + ResultHash []DKGChainResultHash + Seed []*big.Int +} + +// FrostDKGResultChallengedEvent represents a successful FROST DKG challenge. +type FrostDKGResultChallengedEvent struct { + ResultHash DKGChainResultHash + Challenger chain.Address + Reason string + BlockNumber uint64 +} + +// FrostDKGResultApprovedEvent represents a FROST DKG result approval. +type FrostDKGResultApprovedEvent struct { + ResultHash DKGChainResultHash + Approver chain.Address + BlockNumber uint64 +} diff --git a/pkg/tbtc/frost_dkg_coordinator.go b/pkg/tbtc/frost_dkg_coordinator.go new file mode 100644 index 0000000000..dfea4188b6 --- /dev/null +++ b/pkg/tbtc/frost_dkg_coordinator.go @@ -0,0 +1,577 @@ +package tbtc + +import ( + "context" + "fmt" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func initializeFrostDKGCoordinator( + ctx context.Context, + node *node, + frostChain FrostDKGChain, +) { + if frostChain == nil || !frostChain.FrostWalletRegistryAvailable() { + return + } + + frostDeduplicator := newDeduplicator() + + _ = frostChain.OnBridgeNewWalletRequested( + func(event *BridgeNewWalletRequestedEvent) { + logger.Infof( + "observed Bridge NewWalletRequested event at block [%v]; "+ + "waiting for FROST DkgStarted seed callback", + event.BlockNumber, + ) + }, + ) + + _ = frostChain.OnFrostDKGStarted(func(event *FrostDKGStartedEvent) { + go handleFrostDKGStarted( + ctx, + node, + frostChain, + frostDeduplicator, + event, + true, + ) + }) + + _ = frostChain.OnFrostDKGResultSubmitted( + func(event *FrostDKGResultSubmittedEvent) { + go handleFrostDKGResultSubmitted( + ctx, + node, + frostChain, + frostDeduplicator, + event, + ) + }, + ) + + go recoverFrostDKGCoordinatorState(ctx, node, frostChain, frostDeduplicator) +} + +func handleFrostDKGStarted( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + deduplicator *deduplicator, + event *FrostDKGStartedEvent, + waitForConfirmation bool, +) { + if ok := deduplicator.notifyDKGStarted(event.Seed); !ok { + logger.Infof( + "FROST DKG started event with seed [0x%x] has already been processed", + event.Seed, + ) + return + } + + if waitForConfirmation { + confirmationBlock := event.BlockNumber + dkgStartedConfirmationBlocks + logger.Infof( + "observed FROST DKG started event with seed [0x%x] and "+ + "starting block [%v]; waiting for block [%v] to confirm", + event.Seed, + event.BlockNumber, + confirmationBlock, + ) + + if err := node.waitForBlockHeight(ctx, confirmationBlock); err != nil { + logger.Errorf("failed to confirm FROST DKG started event: [%v]", err) + return + } + } + + dkgState, err := frostChain.GetFrostDKGState() + if err != nil { + logger.Errorf("failed to check FROST DKG state: [%v]", err) + return + } + if dkgState != AwaitingResult { + logger.Infof( + "FROST DKG started event with seed [0x%x] and starting "+ + "block [%v] was not confirmed", + event.Seed, + event.BlockNumber, + ) + return + } + + startBlock := uint64(0) + if event.BlockNumber > dkgStartedConfirmationBlocks { + startBlock = event.BlockNumber - dkgStartedConfirmationBlocks + } + + pastEvents, err := frostChain.PastFrostDKGStartedEvents( + &FrostDKGStartedEventFilter{ + StartBlock: startBlock, + }, + ) + if err != nil { + logger.Errorf("failed to get past FROST DKG started events: [%v]", err) + return + } + if len(pastEvents) == 0 { + logger.Errorf("no past FROST DKG started events") + return + } + + lastEvent := pastEvents[len(pastEvents)-1] + memberIndexes, groupSelectionResult, err := localFrostMembership( + node, + frostChain, + ) + if err != nil { + logger.Errorf("failed to resolve FROST DKG membership: [%v]", err) + return + } + + if len(memberIndexes) == 0 { + logger.Infof( + "FROST DKG with seed [0x%x] at block [%v] selected a group "+ + "that does not include this operator; monitoring only", + lastEvent.Seed, + lastEvent.BlockNumber, + ) + return + } + + executeFrostDKGIfPossible( + ctx, + node, + frostChain, + lastEvent, + memberIndexes, + groupSelectionResult, + ) +} + +func recoverFrostDKGCoordinatorState( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + deduplicator *deduplicator, +) { + state, err := frostChain.GetFrostDKGState() + if err != nil { + logger.Errorf("failed to recover FROST DKG state: [%v]", err) + return + } + + switch state { + case AwaitingResult: + startBlock, err := frostDKGRecoveryStartBlock(node, frostChain) + if err != nil { + logger.Errorf("failed to resolve FROST DKG recovery start block: [%v]", err) + return + } + + events, err := frostChain.PastFrostDKGStartedEvents( + &FrostDKGStartedEventFilter{StartBlock: startBlock}, + ) + if err != nil { + logger.Errorf("failed to recover past FROST DKG started events: [%v]", err) + return + } + if len(events) == 0 { + logger.Warnf("FROST DKG state is AwaitingResult but no DkgStarted event was found") + return + } + + handleFrostDKGStarted( + ctx, + node, + frostChain, + deduplicator, + events[len(events)-1], + false, + ) + + case Challenge: + startBlock, err := frostDKGRecoveryStartBlock(node, frostChain) + if err != nil { + logger.Errorf("failed to resolve FROST DKG recovery start block: [%v]", err) + return + } + + events, err := frostChain.PastFrostDKGResultSubmittedEvents( + &FrostDKGResultSubmittedEventFilter{StartBlock: startBlock}, + ) + if err != nil { + logger.Errorf("failed to recover past FROST DKG result submissions: [%v]", err) + return + } + if len(events) == 0 { + logger.Warnf("FROST DKG state is Challenge but no result submission was found") + return + } + + handleFrostDKGResultSubmitted( + ctx, + node, + frostChain, + deduplicator, + events[len(events)-1], + ) + } +} + +func handleFrostDKGResultSubmitted( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + deduplicator *deduplicator, + event *FrostDKGResultSubmittedEvent, +) { + if ok := deduplicator.notifyDKGResultSubmitted( + event.Seed, + event.ResultHash, + event.BlockNumber, + ); !ok { + logger.Infof( + "FROST DKG result with hash [0x%x] for seed [0x%x] at block [%v] "+ + "has already been processed", + event.ResultHash, + event.Seed, + event.BlockNumber, + ) + return + } + + valid, reason, err := frostChain.IsFrostDKGResultValid(event.Result) + if err != nil { + logger.Errorf( + "failed to validate FROST DKG result [0x%x]: [%v]", + event.ResultHash, + err, + ) + return + } + + if !valid { + logger.Warnf( + "challenging invalid FROST DKG result [0x%x]: [%s]", + event.ResultHash, + reason, + ) + challengeInvalidFrostDKGResult(ctx, node, frostChain, event) + return + } + + memberIndexes, _, err := localFrostMembership(node, frostChain) + if err != nil { + logger.Errorf("failed to resolve local FROST DKG membership: [%v]", err) + return + } + if len(memberIndexes) == 0 { + logger.Infof( + "FROST DKG result [0x%x] is valid; this operator is not in the "+ + "selected group and will not approve", + event.ResultHash, + ) + return + } + + params, err := frostChain.FrostDKGParameters() + if err != nil { + logger.Errorf("failed to get FROST DKG parameters: [%v]", err) + return + } + + challengePeriodEndBlock := event.BlockNumber + params.ChallengePeriodBlocks + approvePrecedencePeriodStartBlock := challengePeriodEndBlock + 1 + approvePeriodStartBlock := approvePrecedencePeriodStartBlock + + params.ApprovePrecedencePeriodBlocks + + for _, currentMemberIndex := range memberIndexes { + memberIndex := currentMemberIndex + var approvalBlock uint64 + if uint64(memberIndex) == event.Result.SubmitterMemberIndex { + approvalBlock = approvePrecedencePeriodStartBlock + } else { + approvalBlock = approvePeriodStartBlock + + uint64(memberIndex-1)*dkgResultApprovalDelayStepBlocks + } + + go scheduleFrostDKGResultApproval( + ctx, + node, + frostChain, + event, + memberIndex, + approvalBlock, + ) + } +} + +func challengeInvalidFrostDKGResult( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + event *FrostDKGResultSubmittedEvent, +) { + for attempt := uint64(1); ; attempt++ { + select { + case <-ctx.Done(): + logger.Errorf( + "stopping FROST DKG challenge confirmation: [%v]", + ctx.Err(), + ) + return + default: + } + + state, err := frostChain.GetFrostDKGState() + if err != nil { + logger.Errorf("failed to check FROST DKG state before challenge: [%v]", err) + return + } + if state != Challenge { + logger.Infof( + "invalid FROST DKG result [0x%x] challenged successfully", + event.ResultHash, + ) + return + } + + if err := frostChain.ChallengeFrostDKGResult(event.Result); err != nil { + state, stateErr := frostChain.GetFrostDKGState() + if stateErr == nil && state != Challenge { + logger.Infof( + "invalid FROST DKG result [0x%x] was challenged by another "+ + "operator", + event.ResultHash, + ) + return + } + + logger.Errorf( + "failed to challenge FROST DKG result [0x%x]: [%v]", + event.ResultHash, + err, + ) + if stateErr != nil { + logger.Errorf( + "failed to check FROST DKG state after challenge error: [%v]", + stateErr, + ) + } + return + } + + currentBlock, err := currentFrostDKGBlock(node) + if err != nil { + logger.Errorf( + "failed to get current block after FROST DKG challenge: [%v]", + err, + ) + return + } + + confirmationBlock := currentBlock + dkgResultChallengeConfirmationBlocks + logger.Infof( + "challenging invalid FROST DKG result [0x%x], attempt [%v]; "+ + "waiting for block [%v] to confirm DKG state", + event.ResultHash, + attempt, + confirmationBlock, + ) + + if err := node.waitForBlockHeight(ctx, confirmationBlock); err != nil { + logger.Errorf( + "failed to wait for FROST DKG challenge confirmation: [%v]", + err, + ) + return + } + if ctx.Err() != nil { + logger.Errorf( + "stopping FROST DKG challenge confirmation: [%v]", + ctx.Err(), + ) + return + } + + state, err = frostChain.GetFrostDKGState() + if err != nil { + logger.Errorf("failed to check FROST DKG state after challenge: [%v]", err) + return + } + if state != Challenge { + logger.Infof( + "invalid FROST DKG result [0x%x] challenged successfully", + event.ResultHash, + ) + return + } + + logger.Infof( + "invalid FROST DKG result [0x%x] still not challenged; retrying", + event.ResultHash, + ) + } +} + +func scheduleFrostDKGResultApproval( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + event *FrostDKGResultSubmittedEvent, + memberIndex group.MemberIndex, + approvalBlock uint64, +) { + logger.Infof( + "FROST DKG result [0x%x] is valid; member [%d] scheduling approval "+ + "at block [%v]", + event.ResultHash, + memberIndex, + approvalBlock, + ) + + if err := node.waitForBlockHeight(ctx, approvalBlock); err != nil { + logger.Errorf( + "member [%d] failed to wait for FROST DKG approval block [%v]: [%v]", + memberIndex, + approvalBlock, + err, + ) + return + } + + state, err := frostChain.GetFrostDKGState() + if err != nil { + logger.Errorf("failed to check FROST DKG state before approval: [%v]", err) + return + } + if state != Challenge { + logger.Infof( + "skipping FROST DKG result [0x%x] approval; current state is [%v]", + event.ResultHash, + state, + ) + return + } + + valid, reason, err := frostChain.IsFrostDKGResultValid(event.Result) + if err != nil { + logger.Errorf( + "failed to revalidate FROST DKG result [0x%x] before approval: [%v]", + event.ResultHash, + err, + ) + return + } + if !valid { + logger.Errorf( + "FROST DKG result [0x%x] became invalid before approval: [%s]", + event.ResultHash, + reason, + ) + return + } + + if err := frostChain.ApproveFrostDKGResult(event.Result); err != nil { + logger.Errorf( + "member [%d] failed to approve FROST DKG result [0x%x]: [%v]", + memberIndex, + event.ResultHash, + err, + ) + } +} + +func localFrostMembership( + node *node, + frostChain FrostDKGChain, +) ([]group.MemberIndex, *GroupSelectionResult, error) { + operatorAddress, err := node.operatorAddress() + if err != nil { + return nil, nil, err + } + + groupSelectionResult, err := frostChain.SelectFrostGroup() + if err != nil { + return nil, nil, fmt.Errorf("failed to select FROST group: [%v]", err) + } + + memberIndexes := make([]group.MemberIndex, 0) + for i, selectedOperatorAddress := range groupSelectionResult.OperatorsAddresses { + if selectedOperatorAddress == operatorAddress { + memberIndexes = append(memberIndexes, group.MemberIndex(i+1)) + } + } + + return memberIndexes, groupSelectionResult, nil +} + +func currentFrostDKGBlock(node *node) (uint64, error) { + blockCounter, err := node.chain.BlockCounter() + if err != nil { + return 0, err + } + + return blockCounter.CurrentBlock() +} + +func frostDKGRecoveryStartBlock( + node *node, + frostChain FrostDKGChain, +) (uint64, error) { + currentBlock, err := currentFrostDKGBlock(node) + if err != nil { + return 0, err + } + + params, err := frostChain.FrostDKGParameters() + if err != nil { + return 0, err + } + + lookBackBlocks, err := frostDKGRecoveryLookBackBlocks( + params, + node.groupParameters, + ) + if err != nil { + return 0, err + } + + return boundedFrostDKGRecoveryStartBlock(currentBlock, lookBackBlocks), nil +} + +func frostDKGRecoveryLookBackBlocks( + params *DKGParameters, + groupParameters *GroupParameters, +) (uint64, error) { + if params == nil { + return 0, fmt.Errorf("FROST DKG parameters are nil") + } + if groupParameters == nil { + return 0, fmt.Errorf("group parameters are nil") + } + + // Bound cold-start eth_getLogs by the live on-chain timing parameters while + // still covering the full lifecycle that may require local action after a + // restart: result submission, challenge, submitter precedence, and delayed + // approval fallback across the full group. + return params.SubmissionTimeoutBlocks + + params.ChallengePeriodBlocks + + params.ApprovePrecedencePeriodBlocks + + uint64(groupParameters.GroupSize)*dkgResultApprovalDelayStepBlocks + + dkgStartedConfirmationBlocks, + nil +} + +func boundedFrostDKGRecoveryStartBlock( + currentBlock uint64, + lookBackBlocks uint64, +) uint64 { + if currentBlock <= lookBackBlocks { + return 0 + } + + return currentBlock - lookBackBlocks +} diff --git a/pkg/tbtc/frost_dkg_coordinator_test.go b/pkg/tbtc/frost_dkg_coordinator_test.go new file mode 100644 index 0000000000..9f38c16f8d --- /dev/null +++ b/pkg/tbtc/frost_dkg_coordinator_test.go @@ -0,0 +1,78 @@ +package tbtc + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/keep-network/keep-core/pkg/frost/registry" +) + +func TestChallengeInvalidFrostDKGResultRetriesUntilStateLeavesChallenge(t *testing.T) { + localChain := Connect(time.Millisecond) + node := &node{chain: localChain} + + frostChain := &retryingFrostDKGChallengeChain{ + state: Challenge, + successOnAttempt: 2, + } + + ctx, cancelCtx := context.WithTimeout(context.Background(), time.Second) + defer cancelCtx() + + challengeInvalidFrostDKGResult( + ctx, + node, + frostChain, + &FrostDKGResultSubmittedEvent{ + ResultHash: [32]byte{0x01}, + Result: ®istry.Result{}, + }, + ) + + if frostChain.challengeCount != 2 { + t.Fatalf( + "unexpected challenge count\nexpected: [2]\nactual: [%d]", + frostChain.challengeCount, + ) + } + + state, err := frostChain.GetFrostDKGState() + if err != nil { + t.Fatalf("unexpected state error: [%v]", err) + } + if state == Challenge { + t.Fatal("expected challenge loop to leave Challenge state") + } +} + +type retryingFrostDKGChallengeChain struct { + FrostDKGChain + + mutex sync.Mutex + state DKGState + challengeCount int + successOnAttempt int +} + +func (rfdgcc *retryingFrostDKGChallengeChain) GetFrostDKGState() (DKGState, error) { + rfdgcc.mutex.Lock() + defer rfdgcc.mutex.Unlock() + + return rfdgcc.state, nil +} + +func (rfdgcc *retryingFrostDKGChallengeChain) ChallengeFrostDKGResult( + *registry.Result, +) error { + rfdgcc.mutex.Lock() + defer rfdgcc.mutex.Unlock() + + rfdgcc.challengeCount++ + if rfdgcc.challengeCount >= rfdgcc.successOnAttempt { + rfdgcc.state = Idle + } + + return nil +} diff --git a/pkg/tbtc/frost_dkg_execution_default.go b/pkg/tbtc/frost_dkg_execution_default.go new file mode 100644 index 0000000000..4cbd02f4aa --- /dev/null +++ b/pkg/tbtc/frost_dkg_execution_default.go @@ -0,0 +1,26 @@ +//go:build !frost_native + +package tbtc + +import ( + "context" + + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func executeFrostDKGIfPossible( + _ context.Context, + _ *node, + _ FrostDKGChain, + event *FrostDKGStartedEvent, + memberIndexes []group.MemberIndex, + _ *GroupSelectionResult, +) { + logger.Infof( + "FROST DKG with seed [0x%x] selected this operator as member "+ + "indexes [%v], but native FROST DKG execution is unavailable "+ + "in this build", + event.Seed, + memberIndexes, + ) +} diff --git a/pkg/tbtc/frost_dkg_execution_frost_native.go b/pkg/tbtc/frost_dkg_execution_frost_native.go new file mode 100644 index 0000000000..8a4f3d85ce --- /dev/null +++ b/pkg/tbtc/frost_dkg_execution_frost_native.go @@ -0,0 +1,681 @@ +//go:build frost_native + +package tbtc + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + + "github.com/btcsuite/btcd/btcec/v2" + "go.uber.org/zap" + + "github.com/keep-network/keep-core/pkg/frost" + "github.com/keep-network/keep-core/pkg/frost/registry" + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/net" + protocolannouncer "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" +) + +const frostDKGResultSubmissionDelayStepBlocks = 30 + +func executeFrostDKGIfPossible( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + event *FrostDKGStartedEvent, + memberIndexes []group.MemberIndex, + groupSelectionResult *GroupSelectionResult, +) { + nativeTBTCSignerEngine := frostsigning.CurrentNativeTBTCSignerEngine() + if nativeTBTCSignerEngine == nil { + logger.Infof( + "FROST DKG with seed [0x%x] selected this operator as member "+ + "indexes [%v], but no native tbtc-signer engine is registered", + event.Seed, + memberIndexes, + ) + return + } + + membershipValidator := group.NewMembershipValidator( + logger, + groupSelectionResult.OperatorsAddresses, + node.chain.Signing(), + ) + + channelName := fmt.Sprintf("%s-frost-dkg-%s", ProtocolName, event.Seed.Text(16)) + channel, err := node.netProvider.BroadcastChannelFor(channelName) + if err != nil { + logger.Errorf("failed to get FROST DKG broadcast channel: [%v]", err) + return + } + + registerFrostDKGResultSigningUnmarshaller(channel) + protocolannouncer.RegisterUnmarshaller(channel) + + if err := channel.SetFilter(membershipValidator.IsInGroup); err != nil { + logger.Errorf("failed to set FROST DKG broadcast channel filter: [%v]", err) + return + } + + params, err := frostChain.FrostDKGParameters() + if err != nil { + logger.Errorf("failed to get FROST DKG parameters: [%v]", err) + return + } + + signatureThreshold, err := frostDKGSignatureThreshold(node.groupParameters) + if err != nil { + logger.Errorf("invalid FROST DKG group parameters: [%v]", err) + return + } + + fullMembers := frostFullMembers(groupSelectionResult) + dkgTimeoutBlock := event.BlockNumber + params.SubmissionTimeoutBlocks + + for _, currentMemberIndex := range memberIndexes { + memberIndex := currentMemberIndex + + go func() { + dkgLogger := logger.With( + zap.String("seed", fmt.Sprintf("0x%x", event.Seed)), + zap.Uint8("memberIndex", uint8(memberIndex)), + ) + + node.protocolLatch.Lock() + defer node.protocolLatch.Unlock() + + dkgCtx, cancelDkgCtx := withCancelOnBlock( + ctx, + dkgTimeoutBlock, + node.waitForBlockHeight, + ) + defer cancelDkgCtx() + + sessionID := fmt.Sprintf("%s-%s", channelName, "attempt-1") + activeMemberIndexes, misbehavedMembersIndices, err := + announceFrostDKGReadiness( + dkgCtx, + node, + channel, + membershipValidator, + fmt.Sprintf("%v-%v", ProtocolName, "frost-dkg"), + sessionID, + memberIndex, + len(groupSelectionResult.OperatorsIDs), + ) + if err != nil { + dkgLogger.Errorf("FROST DKG readiness announcement failed: [%v]", err) + return + } + + submitterMemberIndex := lowestLocalActiveMemberIndex( + memberIndexes, + activeMemberIndexes, + ) + if submitterMemberIndex == 0 { + dkgLogger.Infof( + "skipping FROST DKG result assembly; no local member "+ + "index is active in [%v]", + activeMemberIndexes, + ) + return + } + + tbtcSignerMemberIndexes, err := finalFrostDKGMemberIndexes( + activeMemberIndexes, + groupSelectionResult, + node.groupParameters, + ) + if err != nil { + dkgLogger.Errorf("failed to resolve final FROST DKG member indexes: [%v]", err) + return + } + + executionResult, err := executeFrostDKG( + nativeTBTCSignerEngine, + event, + tbtcSignerMemberIndexes, + signatureThreshold, + sessionID, + ) + if err != nil { + dkgLogger.Errorf("FROST DKG execution failed: [%v]", err) + return + } + + if err := registerFrostSignerWithMaterial( + node, + executionResult.outputKey, + executionResult.signerMaterial, + memberIndex, + activeMemberIndexes, + groupSelectionResult, + ); err != nil { + dkgLogger.Errorf("failed to register FROST signer: [%v]", err) + return + } + + unsignedResult, err := registry.AssembleResult( + uint64(submitterMemberIndex), + executionResult.outputKey, + fullMembers, + misbehavedMembersIndices, + nil, + nil, + ) + if err != nil { + dkgLogger.Errorf("failed to assemble unsigned FROST DKG result: [%v]", err) + return + } + + signedResult, err := signAndCollectFrostDKGResultSignatures( + dkgCtx, + node, + frostChain, + channel, + membershipValidator, + sessionID, + event.Seed, + memberIndex, + activeMemberIndexes, + groupSelectionResult, + unsignedResult, + ) + if err != nil { + dkgLogger.Errorf("failed to collect FROST DKG result signatures: [%v]", err) + return + } + + valid, reason, err := frostChain.IsFrostDKGResultValid(signedResult) + if err != nil { + dkgLogger.Errorf("failed to pre-validate FROST DKG result: [%v]", err) + return + } + if !valid { + dkgLogger.Errorf("assembled FROST DKG result is invalid: [%s]", reason) + return + } + + if memberIndex != submitterMemberIndex { + dkgLogger.Infof( + "skipping FROST DKG result submission; member [%d] is "+ + "the designated local submitter", + submitterMemberIndex, + ) + return + } + + if err := submitFrostDKGResultWithDelay( + dkgCtx, + node, + frostChain, + submitterMemberIndex, + activeMemberIndexes, + signedResult, + ); err != nil { + dkgLogger.Errorf("failed to submit FROST DKG result: [%v]", err) + return + } + }() + } +} + +type frostDKGExecutionResult struct { + outputKey frost.OutputKey + signerMaterial *frostsigning.NativeSignerMaterial +} + +func executeFrostDKG( + nativeTBTCSignerEngine frostsigning.NativeTBTCSignerEngine, + event *FrostDKGStartedEvent, + dkgMemberIndexes []group.MemberIndex, + signatureThreshold int, + sessionID string, +) (*frostDKGExecutionResult, error) { + if nativeTBTCSignerEngine != nil { + return executeTBTCSignerFROSTDKG( + nativeTBTCSignerEngine, + event, + dkgMemberIndexes, + signatureThreshold, + sessionID, + ) + } + + return nil, fmt.Errorf("native tbtc-signer engine is unavailable") +} + +func finalFrostDKGMemberIndexes( + activeMemberIndexes []group.MemberIndex, + groupSelectionResult *GroupSelectionResult, + groupParameters *GroupParameters, +) ([]group.MemberIndex, error) { + if groupSelectionResult == nil { + return nil, fmt.Errorf("group selection result is nil") + } + if groupParameters == nil { + return nil, fmt.Errorf("group parameters are nil") + } + + operatingMembersIndexes := append([]group.MemberIndex{}, activeMemberIndexes...) + _, finalSigningGroupMembersIndexes, err := finalSigningGroup( + groupSelectionResult.OperatorsAddresses, + operatingMembersIndexes, + groupParameters, + ) + if err != nil { + return nil, err + } + + dkgMemberIndexes := make( + []group.MemberIndex, + 0, + len(operatingMembersIndexes), + ) + for _, activeMemberIndex := range operatingMembersIndexes { + finalMemberIndex, ok := + finalSigningGroupMembersIndexes[activeMemberIndex] + if !ok { + return nil, fmt.Errorf( + "active member [%d] is missing final FROST DKG member index", + activeMemberIndex, + ) + } + + dkgMemberIndexes = append(dkgMemberIndexes, finalMemberIndex) + } + + return dkgMemberIndexes, nil +} + +func executeTBTCSignerFROSTDKG( + nativeEngine frostsigning.NativeTBTCSignerEngine, + event *FrostDKGStartedEvent, + dkgMemberIndexes []group.MemberIndex, + signatureThreshold int, + sessionID string, +) (*frostDKGExecutionResult, error) { + if nativeEngine == nil { + return nil, fmt.Errorf("native tbtc-signer engine is unavailable") + } + + seededEngine, ok := nativeEngine.(frostsigning.NativeTBTCSignerSeededDKGEngine) + if !ok { + return nil, fmt.Errorf("native tbtc-signer engine does not support seeded DKG") + } + + dkgSeedHex, err := frostDKGSeedHex(event.Seed) + if err != nil { + return nil, err + } + + participants, err := nativeTBTCSignerDKGParticipants(dkgMemberIndexes) + if err != nil { + return nil, err + } + + if signatureThreshold <= 0 || signatureThreshold > int(^uint16(0)) { + return nil, fmt.Errorf( + "invalid tbtc-signer DKG threshold [%d]", + signatureThreshold, + ) + } + + dkgResult, err := seededEngine.RunDKGWithSeed( + sessionID, + participants, + uint16(signatureThreshold), + dkgSeedHex, + ) + if err != nil { + return nil, fmt.Errorf("tbtc-signer RunDKG failed: [%w]", err) + } + + outputKey, err := outputKeyFromTBTCSignerDKGResult(dkgResult) + if err != nil { + return nil, err + } + + payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ + KeyGroup: dkgResult.KeyGroup, + TaprootOutputKey: hex.EncodeToString(outputKey[:]), + KeyGroupSource: frostsigning.NativeTBTCSignerKeyGroupSourceDKGPersisted, + DKGSeedHex: dkgSeedHex, + DKGParticipants: participants, + DKGThreshold: uint16(signatureThreshold), + }) + if err != nil { + return nil, fmt.Errorf("cannot marshal tbtc-signer material: [%w]", err) + } + + return &frostDKGExecutionResult{ + outputKey: outputKey, + signerMaterial: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + }, + }, nil +} + +func nativeTBTCSignerDKGParticipants( + activeMemberIndexes []group.MemberIndex, +) ([]frostsigning.NativeTBTCSignerDKGParticipant, error) { + participants := make( + []frostsigning.NativeTBTCSignerDKGParticipant, + 0, + len(activeMemberIndexes), + ) + + for _, memberIndex := range activeMemberIndexes { + if memberIndex == 0 { + return nil, fmt.Errorf( + "invalid tbtc-signer DKG member index [%d]", + memberIndex, + ) + } + + identifier := uint16(memberIndex) + participants = append( + participants, + frostsigning.NativeTBTCSignerDKGParticipant{ + Identifier: identifier, + PublicKeyHex: frostsigning. + NativeTBTCSignerDKGPlaceholderPublicKeyHex(identifier), + }, + ) + } + + return participants, nil +} + +func frostDKGSeedHex(seed *big.Int) (string, error) { + if seed == nil { + return "", fmt.Errorf("FROST DKG seed is nil") + } + if seed.Sign() < 0 || len(seed.Bytes()) > frost.OutputKeySize { + return "", fmt.Errorf("FROST DKG seed must fit in %d bytes", frost.OutputKeySize) + } + + seedBytes := make([]byte, frost.OutputKeySize) + seed.FillBytes(seedBytes) + + return hex.EncodeToString(seedBytes), nil +} + +func outputKeyFromTBTCSignerDKGResult( + dkgResult *frostsigning.NativeTBTCSignerDKGResult, +) (frost.OutputKey, error) { + if dkgResult == nil { + return frost.OutputKey{}, fmt.Errorf("tbtc-signer DKG result is nil") + } + if dkgResult.KeyGroup == "" { + return frost.OutputKey{}, fmt.Errorf("tbtc-signer DKG key group is empty") + } + + outputKeyBytes, err := frostsigning.TaprootOutputKeyFromTBTCSignerKey( + dkgResult.KeyGroup, + ) + if err != nil { + return frost.OutputKey{}, fmt.Errorf( + "cannot derive tbtc-signer DKG Taproot output key: [%w]", + err, + ) + } + if len(outputKeyBytes) != frost.OutputKeySize { + return frost.OutputKey{}, fmt.Errorf( + "unexpected tbtc-signer DKG output key length [%d]", + len(outputKeyBytes), + ) + } + + var outputKey frost.OutputKey + copy(outputKey[:], outputKeyBytes) + + return outputKey, nil +} + +func announceFrostDKGReadiness( + ctx context.Context, + node *node, + channel net.BroadcastChannel, + membershipValidator *group.MembershipValidator, + protocolID string, + sessionID string, + memberIndex group.MemberIndex, + groupSize int, +) ( + []group.MemberIndex, + registry.MisbehavedMemberIndices, + error, +) { + blockCounter, err := node.chain.BlockCounter() + if err != nil { + return nil, nil, err + } + + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return nil, nil, err + } + + announcementEndBlock := currentBlock + dkgAttemptAnnouncementActiveBlocks + announceCtx, cancelAnnounceCtx := withCancelOnBlock( + ctx, + announcementEndBlock, + node.waitForBlockHeight, + ) + defer cancelAnnounceCtx() + + announcer := protocolannouncer.New(protocolID, channel, membershipValidator) + activeMemberIndexes, err := announcer.Announce( + announceCtx, + memberIndex, + sessionID, + ) + if err != nil { + return nil, nil, err + } + if ctx.Err() != nil { + return nil, nil, ctx.Err() + } + + if len(activeMemberIndexes) < node.groupParameters.GroupQuorum { + return nil, nil, fmt.Errorf( + "FROST DKG readiness quorum not reached: [%d] active members, quorum [%d]", + len(activeMemberIndexes), + node.groupParameters.GroupQuorum, + ) + } + + return activeMemberIndexes, + frostMisbehavedMemberIndices(groupSize, activeMemberIndexes), + nil +} + +func registerFrostSignerWithMaterial( + node *node, + outputKey frost.OutputKey, + signerMaterial *frostsigning.NativeSignerMaterial, + memberIndex group.MemberIndex, + activeMemberIndexes []group.MemberIndex, + groupSelectionResult *GroupSelectionResult, +) error { + if signerMaterial == nil { + return fmt.Errorf("FROST signer material is nil") + } + + walletPublicKey, err := frostOutputKeyToECDSAPublicKey(outputKey) + if err != nil { + return err + } + + finalSigningGroupOperators, finalSigningGroupMembersIndexes, err := + finalSigningGroup( + groupSelectionResult.OperatorsAddresses, + append([]group.MemberIndex{}, activeMemberIndexes...), + node.groupParameters, + ) + if err != nil { + return fmt.Errorf("failed to resolve final FROST signing group members: [%w]", err) + } + + finalSigningGroupMemberIndex, ok := + finalSigningGroupMembersIndexes[memberIndex] + if !ok { + return fmt.Errorf("failed to resolve final FROST signing group member index") + } + + return node.walletRegistry.registerSigner(newSigner( + walletPublicKey, + finalSigningGroupOperators, + finalSigningGroupMemberIndex, + nil, + signerMaterial, + )) +} + +func submitFrostDKGResultWithDelay( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + memberIndex group.MemberIndex, + activeMemberIndexes []group.MemberIndex, + result *registry.Result, +) error { + rank := -1 + for i, activeMemberIndex := range activeMemberIndexes { + if activeMemberIndex == memberIndex { + rank = i + break + } + } + if rank < 0 { + return fmt.Errorf( + "FROST DKG submitter member [%d] is not in active members [%v]", + memberIndex, + activeMemberIndexes, + ) + } + + blockCounter, err := node.chain.BlockCounter() + if err != nil { + return err + } + + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return err + } + + submissionBlock := currentBlock + + uint64(rank)*frostDKGResultSubmissionDelayStepBlocks + if err := node.waitForBlockHeight(ctx, submissionBlock); err != nil { + return err + } + if ctx.Err() != nil { + return ctx.Err() + } + + state, err := frostChain.GetFrostDKGState() + if err != nil { + return err + } + if state != AwaitingResult { + logger.Infof( + "skipping FROST DKG result submission by member [%d]; current state is [%v]", + memberIndex, + state, + ) + return nil + } + + return frostChain.SubmitFrostDKGResult(result) +} + +func frostOutputKeyToECDSAPublicKey( + outputKey frost.OutputKey, +) (*ecdsa.PublicKey, error) { + compressed := make([]byte, 0, 1+frost.OutputKeySize) + compressed = append(compressed, byte(0x02)) + compressed = append(compressed, outputKey[:]...) + + publicKey, err := btcec.ParsePubKey(compressed) + if err != nil { + return nil, fmt.Errorf("cannot lift x-only FROST output key: [%w]", err) + } + + return &ecdsa.PublicKey{ + Curve: tecdsa.Curve, + X: publicKey.X(), + Y: publicKey.Y(), + }, nil +} + +func frostFullMembers( + groupSelectionResult *GroupSelectionResult, +) registry.FullMembers { + members := make(registry.FullMembers, len(groupSelectionResult.OperatorsIDs)) + for i, operatorID := range groupSelectionResult.OperatorsIDs { + members[i] = uint32(operatorID) + } + + return members +} + +func lowestLocalActiveMemberIndex( + localMemberIndexes []group.MemberIndex, + activeMemberIndexes []group.MemberIndex, +) group.MemberIndex { + activeMembersSet := make( + map[group.MemberIndex]struct{}, + len(activeMemberIndexes), + ) + for _, activeMemberIndex := range activeMemberIndexes { + activeMembersSet[activeMemberIndex] = struct{}{} + } + + var lowest group.MemberIndex + for _, localMemberIndex := range localMemberIndexes { + if _, ok := activeMembersSet[localMemberIndex]; !ok { + continue + } + + if lowest == 0 || localMemberIndex < lowest { + lowest = localMemberIndex + } + } + + return lowest +} + +func frostMisbehavedMemberIndices( + groupSize int, + activeMemberIndexes []group.MemberIndex, +) registry.MisbehavedMemberIndices { + activeMembersSet := make(map[group.MemberIndex]struct{}, len(activeMemberIndexes)) + for _, memberIndex := range activeMemberIndexes { + activeMembersSet[memberIndex] = struct{}{} + } + + misbehavedMembersIndices := make(registry.MisbehavedMemberIndices, 0) + for i := 1; i <= groupSize; i++ { + memberIndex := group.MemberIndex(i) + if _, ok := activeMembersSet[memberIndex]; ok { + continue + } + + misbehavedMembersIndices = append( + misbehavedMembersIndices, + uint8(memberIndex), + ) + } + + return misbehavedMembersIndices +} diff --git a/pkg/tbtc/frost_dkg_execution_frost_native_test.go b/pkg/tbtc/frost_dkg_execution_frost_native_test.go new file mode 100644 index 0000000000..1afb0be72f --- /dev/null +++ b/pkg/tbtc/frost_dkg_execution_frost_native_test.go @@ -0,0 +1,326 @@ +//go:build frost_native + +package tbtc + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "strings" + "testing" + + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost/registry" + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestLowestLocalActiveMemberIndex(t *testing.T) { + testCases := map[string]struct { + local []group.MemberIndex + active []group.MemberIndex + expected group.MemberIndex + }{ + "lowest local slot active": { + local: []group.MemberIndex{2, 4, 6}, + active: []group.MemberIndex{1, 2, 3, 4}, + expected: 2, + }, + "lowest local slot dropped out": { + local: []group.MemberIndex{2, 4, 6}, + active: []group.MemberIndex{1, 3, 4, 6}, + expected: 4, + }, + "no local slot active": { + local: []group.MemberIndex{2, 4}, + active: []group.MemberIndex{1, 3, 5}, + expected: 0, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + actual := lowestLocalActiveMemberIndex(test.local, test.active) + if actual != test.expected { + t.Fatalf( + "unexpected lowest local active member index\nexpected: [%d]\nactual: [%d]", + test.expected, + actual, + ) + } + }) + } +} + +func TestFrostMisbehavedMemberIndices(t *testing.T) { + actual := frostMisbehavedMemberIndices( + 7, + []group.MemberIndex{1, 3, 4, 7}, + ) + + expected := registry.MisbehavedMemberIndices{2, 5, 6} + if len(actual) != len(expected) { + t.Fatalf( + "unexpected misbehaved member indices length\nexpected: [%d]\nactual: [%d]", + len(expected), + len(actual), + ) + } + for i := range expected { + if actual[i] != expected[i] { + t.Fatalf( + "unexpected misbehaved member index at [%d]\nexpected: [%d]\nactual: [%d]", + i, + expected[i], + actual[i], + ) + } + } +} + +func TestOutputKeyFromTBTCSignerDKGResult_AcceptsCompressedKeyGroup( + t *testing.T, +) { + const compressedKey = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + const xOnlyKey = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + + outputKey, err := outputKeyFromTBTCSignerDKGResult( + &frostsigning.NativeTBTCSignerDKGResult{ + KeyGroup: compressedKey, + }, + ) + if err != nil { + t.Fatalf("output key: %v", err) + } + + want, _ := hex.DecodeString(xOnlyKey) + if !bytes.Equal(outputKey[:], want) { + t.Fatalf( + "unexpected output key\nexpected: [%x]\nactual: [%x]", + want, + outputKey[:], + ) + } +} + +func TestExecuteFrostDKG_UsesTBTCSignerMaterial(t *testing.T) { + tbtcSignerEngine := &testNativeTBTCSignerSeededDKGEngine{} + + result, err := executeFrostDKG( + tbtcSignerEngine, + &FrostDKGStartedEvent{Seed: big.NewInt(0x1234)}, + []group.MemberIndex{1, 2, 3}, + 2, + "test-session", + ) + if err != nil { + t.Fatalf("unexpected DKG error: [%v]", err) + } + + if !tbtcSignerEngine.runDKGWithSeedCalled { + t.Fatal("expected tbtc-signer DKG engine to be used") + } + if result.signerMaterial == nil { + t.Fatal("expected signer material") + } + assertTBTCSignerDKGParticipantIdentifiers( + t, + tbtcSignerEngine.runDKGWithSeedParticipants, + []uint16{1, 2, 3}, + ) + if result.signerMaterial.Format != + frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1 { + t.Fatalf( + "unexpected signer material format\nexpected: [%s]\nactual: [%s]", + frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + result.signerMaterial.Format, + ) + } + + var payload frostsigning.NativeTBTCSignerMaterialPayload + if err := json.Unmarshal(result.signerMaterial.Payload, &payload); err != nil { + t.Fatalf("unexpected signer material payload decode error: [%v]", err) + } + assertTBTCSignerDKGParticipantIdentifiers( + t, + payload.DKGParticipants, + []uint16{1, 2, 3}, + ) +} + +func TestFinalFrostDKGMemberIndexes_NormalizesToFinalSigningGroupIndexes( + t *testing.T, +) { + activeMemberIndexes := []group.MemberIndex{5, 2, 4} + + actual, err := finalFrostDKGMemberIndexes( + activeMemberIndexes, + &GroupSelectionResult{ + OperatorsAddresses: chain.Addresses{ + "0xAA", + "0xBB", + "0xCC", + "0xDD", + "0xEE", + }, + }, + &GroupParameters{ + GroupSize: 5, + GroupQuorum: 3, + HonestThreshold: 2, + }, + ) + if err != nil { + t.Fatalf("unexpected final member index error: [%v]", err) + } + + expected := []group.MemberIndex{1, 2, 3} + if len(actual) != len(expected) { + t.Fatalf( + "unexpected final member indexes count\nexpected: [%d]\nactual: [%d]", + len(expected), + len(actual), + ) + } + for i := range expected { + if actual[i] != expected[i] { + t.Fatalf( + "unexpected final member index at [%d]\nexpected: [%d]\nactual: [%d]", + i, + expected[i], + actual[i], + ) + } + } + + expectedActive := []group.MemberIndex{5, 2, 4} + for i := range expectedActive { + if activeMemberIndexes[i] != expectedActive[i] { + t.Fatalf( + "active member indexes should not be mutated\nexpected: [%v]\nactual: [%v]", + expectedActive, + activeMemberIndexes, + ) + } + } +} + +func TestExecuteFrostDKG_RequiresTBTCSignerMaterial(t *testing.T) { + _, err := executeFrostDKG( + nil, + &FrostDKGStartedEvent{Seed: big.NewInt(0x1234)}, + []group.MemberIndex{1, 2, 3}, + 2, + "test-session", + ) + if err == nil { + t.Fatal("expected missing tbtc-signer engine error") + } + if !strings.Contains(err.Error(), "native tbtc-signer engine is unavailable") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +type testNativeTBTCSignerSeededDKGEngine struct { + runDKGWithSeedCalled bool + runDKGWithSeedParticipants []frostsigning.NativeTBTCSignerDKGParticipant +} + +func (tntsde *testNativeTBTCSignerSeededDKGEngine) RunDKG( + string, + []frostsigning.NativeTBTCSignerDKGParticipant, + uint16, +) (*frostsigning.NativeTBTCSignerDKGResult, error) { + return nil, fmt.Errorf("unseeded RunDKG should not be used") +} + +func (tntsde *testNativeTBTCSignerSeededDKGEngine) RunDKGWithSeed( + sessionID string, + participants []frostsigning.NativeTBTCSignerDKGParticipant, + threshold uint16, + dkgSeedHex string, +) (*frostsigning.NativeTBTCSignerDKGResult, error) { + tntsde.runDKGWithSeedCalled = true + tntsde.runDKGWithSeedParticipants = append( + []frostsigning.NativeTBTCSignerDKGParticipant{}, + participants..., + ) + + if sessionID != "test-session" { + return nil, fmt.Errorf("unexpected session ID: [%s]", sessionID) + } + if len(participants) != 3 { + return nil, fmt.Errorf("unexpected participant count: [%d]", len(participants)) + } + if threshold != 2 { + return nil, fmt.Errorf("unexpected threshold: [%d]", threshold) + } + if dkgSeedHex == "" { + return nil, fmt.Errorf("expected DKG seed") + } + + return &frostsigning.NativeTBTCSignerDKGResult{ + SessionID: sessionID, + KeyGroup: "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ParticipantCount: uint16(len(participants)), + Threshold: threshold, + CreatedAtUnix: 1, + }, nil +} + +func (tntsde *testNativeTBTCSignerSeededDKGEngine) StartSignRound( + string, + uint16, + []byte, + string, + []uint16, + *[32]byte, +) (*frostsigning.NativeTBTCSignerRoundState, error) { + return nil, fmt.Errorf("StartSignRound should not be used") +} + +func (tntsde *testNativeTBTCSignerSeededDKGEngine) FinalizeSignRound( + string, + []frostsigning.NativeTBTCSignerRoundContribution, + *[32]byte, +) ([]byte, error) { + return nil, fmt.Errorf("FinalizeSignRound should not be used") +} + +func (tntsde *testNativeTBTCSignerSeededDKGEngine) BuildTaprootTx( + string, + []frostsigning.NativeTBTCSignerTxInput, + []frostsigning.NativeTBTCSignerTxOutput, + *string, +) (*frostsigning.NativeTBTCSignerTxResult, error) { + return nil, fmt.Errorf("BuildTaprootTx should not be used") +} + +func assertTBTCSignerDKGParticipantIdentifiers( + t *testing.T, + participants []frostsigning.NativeTBTCSignerDKGParticipant, + expected []uint16, +) { + t.Helper() + + if len(participants) != len(expected) { + t.Fatalf( + "unexpected participant count\nexpected: [%d]\nactual: [%d]", + len(expected), + len(participants), + ) + } + + for i := range expected { + if participants[i].Identifier != expected[i] { + t.Fatalf( + "unexpected participant identifier at [%d]\nexpected: [%d]\nactual: [%d]", + i, + expected[i], + participants[i].Identifier, + ) + } + } +} diff --git a/pkg/tbtc/frost_dkg_parameters.go b/pkg/tbtc/frost_dkg_parameters.go new file mode 100644 index 0000000000..987e2b44ab --- /dev/null +++ b/pkg/tbtc/frost_dkg_parameters.go @@ -0,0 +1,26 @@ +package tbtc + +import "fmt" + +func frostDKGSignatureThreshold(groupParameters *GroupParameters) (int, error) { + if groupParameters == nil { + return 0, fmt.Errorf("group parameters are nil") + } + + // The on-chain FROST validator names this value groupThreshold. In keep-core + // group parameters, that is the honest signing threshold, not the ECDSA DKG + // active-participant quorum. + threshold := groupParameters.HonestThreshold + if threshold <= 0 { + return 0, fmt.Errorf("FROST DKG signature threshold must be positive") + } + if threshold > groupParameters.GroupSize { + return 0, fmt.Errorf( + "FROST DKG signature threshold [%d] exceeds group size [%d]", + threshold, + groupParameters.GroupSize, + ) + } + + return threshold, nil +} diff --git a/pkg/tbtc/frost_dkg_parameters_test.go b/pkg/tbtc/frost_dkg_parameters_test.go new file mode 100644 index 0000000000..c97cd6158a --- /dev/null +++ b/pkg/tbtc/frost_dkg_parameters_test.go @@ -0,0 +1,105 @@ +package tbtc + +import "testing" + +func TestFrostDKGSignatureThresholdUsesHonestThreshold(t *testing.T) { + params := &GroupParameters{ + GroupSize: 100, + GroupQuorum: 90, + HonestThreshold: 51, + } + + threshold, err := frostDKGSignatureThreshold(params) + if err != nil { + t.Fatalf("unexpected threshold error: [%v]", err) + } + if threshold != 51 { + t.Fatalf("unexpected threshold\nexpected: [51]\nactual: [%d]", threshold) + } +} + +func TestFrostDKGSignatureThresholdRejectsInvalidParameters(t *testing.T) { + testCases := map[string]*GroupParameters{ + "nil": nil, + "zero threshold": {GroupSize: 100, GroupQuorum: 90, HonestThreshold: 0}, + "above group size": {GroupSize: 3, GroupQuorum: 3, HonestThreshold: 4}, + } + + for name, params := range testCases { + t.Run(name, func(t *testing.T) { + _, err := frostDKGSignatureThreshold(params) + if err == nil { + t.Fatal("expected error") + } + }) + } +} + +func TestBoundedFrostDKGRecoveryStartBlock(t *testing.T) { + lookBackBlocks := uint64(13560) + + testCases := map[string]struct { + currentBlock uint64 + expected uint64 + }{ + "below lookback": { + currentBlock: 100, + expected: 0, + }, + "equal lookback": { + currentBlock: lookBackBlocks, + expected: 0, + }, + "one block above lookback": { + currentBlock: lookBackBlocks + 1, + expected: 1, + }, + "above lookback": { + currentBlock: lookBackBlocks + 123, + expected: 123, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + actual := boundedFrostDKGRecoveryStartBlock( + test.currentBlock, + lookBackBlocks, + ) + if actual != test.expected { + t.Fatalf( + "unexpected start block\nexpected: [%d]\nactual: [%d]", + test.expected, + actual, + ) + } + }) + } +} + +func TestFrostDKGRecoveryLookBackBlocksCoversFullLifecycle(t *testing.T) { + params := &DKGParameters{ + SubmissionTimeoutBlocks: 500, + ChallengePeriodBlocks: 11520, + ApprovePrecedencePeriodBlocks: 20, + } + groupParameters := &GroupParameters{ + GroupSize: 100, + GroupQuorum: 90, + HonestThreshold: 51, + } + + actual, err := frostDKGRecoveryLookBackBlocks(params, groupParameters) + if err != nil { + t.Fatalf("unexpected lookback error: [%v]", err) + } + + expected := uint64(500 + 11520 + 20 + 100*dkgResultApprovalDelayStepBlocks + dkgStartedConfirmationBlocks) + if actual != expected { + t.Fatalf( + "unexpected lookback\nexpected: [%d]\nactual: [%d]", + expected, + actual, + ) + } +} diff --git a/pkg/tbtc/frost_dkg_result_signing.go b/pkg/tbtc/frost_dkg_result_signing.go new file mode 100644 index 0000000000..5c80e7eca8 --- /dev/null +++ b/pkg/tbtc/frost_dkg_result_signing.go @@ -0,0 +1,279 @@ +package tbtc + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "math/big" + "sort" + + "github.com/keep-network/keep-core/pkg/frost/registry" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +const frostDKGResultSigningMessageTypePrefix = "frost_dkg/result_signing/" + +type frostDKGResultSignatureMessage struct { + SenderIDValue uint32 `json:"senderID"` + SessionID string `json:"sessionID"` + Digest []byte `json:"digest"` + PublicKey []byte `json:"publicKey"` + Signature []byte `json:"signature"` +} + +func (fdrsm *frostDKGResultSignatureMessage) SenderID() group.MemberIndex { + return group.MemberIndex(fdrsm.SenderIDValue) +} + +func (fdrsm *frostDKGResultSignatureMessage) Type() string { + return frostDKGResultSigningMessageTypePrefix + "signature" +} + +func (fdrsm *frostDKGResultSignatureMessage) Marshal() ([]byte, error) { + return json.Marshal(fdrsm) +} + +func (fdrsm *frostDKGResultSignatureMessage) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, fdrsm); err != nil { + return err + } + + if fdrsm.SenderID() == 0 { + return fmt.Errorf("sender ID is zero") + } + if fdrsm.SessionID == "" { + return fmt.Errorf("session ID is empty") + } + if len(fdrsm.Digest) != 32 { + return fmt.Errorf("digest length [%d] is not 32", len(fdrsm.Digest)) + } + if len(fdrsm.PublicKey) == 0 { + return fmt.Errorf("public key is empty") + } + if len(fdrsm.Signature) == 0 { + return fmt.Errorf("signature is empty") + } + + return nil +} + +func registerFrostDKGResultSigningUnmarshaller(channel net.BroadcastChannel) { + channel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &frostDKGResultSignatureMessage{} + }) +} + +func signAndCollectFrostDKGResultSignatures( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + channel net.BroadcastChannel, + membershipValidator *group.MembershipValidator, + sessionID string, + seed *big.Int, + memberIndex group.MemberIndex, + includedMembersIndexes []group.MemberIndex, + groupSelectionResult *GroupSelectionResult, + unsignedResult *registry.Result, +) (*registry.Result, error) { + if unsignedResult == nil { + return nil, fmt.Errorf("unsigned FROST DKG result is nil") + } + + includedMembersSet := make(map[group.MemberIndex]struct{}) + for _, includedMemberIndex := range includedMembersIndexes { + includedMembersSet[includedMemberIndex] = struct{}{} + } + + digest, err := frostChain.CalculateFrostDKGResultDigest(seed, unsignedResult) + if err != nil { + return nil, fmt.Errorf("cannot calculate FROST DKG result digest: [%w]", err) + } + + signing := node.chain.Signing() + ownSignature, err := signing.Sign(digest[:]) + if err != nil { + return nil, fmt.Errorf("cannot sign FROST DKG result digest: [%w]", err) + } + + ownMessage := &frostDKGResultSignatureMessage{ + SenderIDValue: uint32(memberIndex), + SessionID: sessionID, + Digest: append([]byte{}, digest[:]...), + PublicKey: append([]byte{}, signing.PublicKey()...), + Signature: append([]byte{}, ownSignature...), + } + if err := channel.Send( + ctx, + ownMessage, + net.BackoffRetransmissionStrategy, + ); err != nil { + return nil, fmt.Errorf("cannot broadcast FROST DKG result signature: [%w]", err) + } + + signatures := map[group.MemberIndex][]byte{ + memberIndex: ownSignature, + } + + expectedSignaturesCount, err := frostDKGSignatureThreshold(node.groupParameters) + if err != nil { + return nil, err + } + if expectedSignaturesCount > len(includedMembersIndexes) { + return nil, fmt.Errorf( + "FROST DKG included members count [%d] is below signature threshold [%d]", + len(includedMembersIndexes), + expectedSignaturesCount, + ) + } + + recvCtx, cancelRecvCtx := context.WithCancel(ctx) + defer cancelRecvCtx() + + // Allow a few rounds of duplicate/replayed signatures without blocking the + // network callback while the collector validates and deduplicates messages. + messageChan := make( + chan *frostDKGResultSignatureMessage, + len(includedMembersIndexes)*4+1, + ) + channel.Recv(recvCtx, func(message net.Message) { + payload, ok := message.Payload().(*frostDKGResultSignatureMessage) + if !ok { + return + } + + if !shouldAcceptFrostDKGResultSignatureMessage( + payload, + message.SenderPublicKey(), + sessionID, + memberIndex, + includedMembersSet, + membershipValidator, + ) { + return + } + + select { + case messageChan <- payload: + default: + logger.Warnf( + "dropping FROST DKG result signature from member [%d]; collector buffer full", + payload.SenderID(), + ) + } + }) + + for len(signatures) < expectedSignaturesCount { + select { + case <-ctx.Done(): + return nil, fmt.Errorf( + "FROST DKG result signature collection interrupted: [%w]", + ctx.Err(), + ) + case message := <-messageChan: + senderID := message.SenderID() + if !bytes.Equal(message.Digest, digest[:]) { + logger.Warnf( + "dropping FROST DKG result signature from member [%d]; digest mismatch", + senderID, + ) + continue + } + + valid, err := signing.VerifyWithPublicKey( + digest[:], + message.Signature, + message.PublicKey, + ) + if err != nil || !valid { + logger.Warnf( + "dropping invalid FROST DKG result signature from member [%d]: [%v]", + senderID, + err, + ) + continue + } + + expectedOperator := groupSelectionResult.OperatorsAddresses[senderID-1] + actualOperator := signing.PublicKeyBytesToAddress(message.PublicKey) + if actualOperator != expectedOperator { + logger.Warnf( + "dropping FROST DKG result signature from member [%d]; "+ + "operator address [%s] does not match selected operator [%s]", + senderID, + actualOperator, + expectedOperator, + ) + continue + } + + if existing, ok := signatures[senderID]; ok { + if !bytes.Equal(existing, message.Signature) { + logger.Warnf( + "dropping conflicting FROST DKG result signature from member [%d]", + senderID, + ) + } + continue + } + + signatures[senderID] = append([]byte{}, message.Signature...) + } + } + + signingMembersIndices := make([]uint64, 0, len(signatures)) + for memberIndex := range signatures { + signingMembersIndices = append(signingMembersIndices, uint64(memberIndex)) + } + sort.Slice(signingMembersIndices, func(i, j int) bool { + return signingMembersIndices[i] < signingMembersIndices[j] + }) + + packedSignatures := make([]byte, 0) + for _, signingMemberIndex := range signingMembersIndices { + packedSignatures = append( + packedSignatures, + signatures[group.MemberIndex(signingMemberIndex)]..., + ) + } + + return registry.AssembleResult( + unsignedResult.SubmitterMemberIndex, + unsignedResult.XOnlyOutputKey, + unsignedResult.Members, + unsignedResult.MisbehavedMembersIndices, + packedSignatures, + signingMembersIndices, + ) +} + +func shouldAcceptFrostDKGResultSignatureMessage( + message *frostDKGResultSignatureMessage, + senderPublicKey []byte, + sessionID string, + selfMemberIndex group.MemberIndex, + includedMembersSet map[group.MemberIndex]struct{}, + membershipValidator *group.MembershipValidator, +) bool { + if message == nil { + return false + } + + senderID := message.SenderID() + if senderID == 0 || senderID == selfMemberIndex { + return false + } + if message.SessionID != sessionID { + return false + } + if _, included := includedMembersSet[senderID]; !included { + return false + } + if membershipValidator == nil { + return true + } + + return membershipValidator.IsValidMembership(senderID, senderPublicKey) +} 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..babbfb34f7 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" @@ -42,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, }) } @@ -72,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{ @@ -82,7 +88,8 @@ func (s *signer) Unmarshal(bytes []byte) error { signingGroupOperators: walletSigningGroupOperators, } s.signingGroupMemberIndex = group.MemberIndex(pbSigner.SigningGroupMemberIndex) - s.privateKeyShare = privateKeyShare + s.privateKeyShare = signerMaterial.privateKeyShare + s.signerMaterial = signerMaterial.signerMaterial return nil } @@ -114,7 +121,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..c1e750f9ec 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) { @@ -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) { @@ -53,12 +51,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 +72,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.go b/pkg/tbtc/moved_funds_sweep.go index 2569f4557d..8c3e809133 100644 --- a/pkg/tbtc/moved_funds_sweep.go +++ b/pkg/tbtc/moved_funds_sweep.go @@ -161,8 +161,8 @@ func (mfsa *movedFundsSweepAction) execute() error { } // Prepare the wallet's main UTXO. - walletMainUtxo, err := DetermineWalletMainUtxo( - walletPublicKeyHash, + walletMainUtxo, err := DetermineWalletMainUtxoForPublicKey( + mfsa.wallet().publicKey, mfsa.chain, mfsa.btcChain, ) @@ -173,8 +173,8 @@ func (mfsa *movedFundsSweepAction) execute() error { ) } - err = EnsureWalletSyncedBetweenChains( - walletPublicKeyHash, + err = EnsureWalletSyncedBetweenChainsForPublicKey( + mfsa.wallet().publicKey, walletMainUtxo, mfsa.chain, mfsa.btcChain, @@ -318,6 +318,27 @@ func assembleMovedFundsSweepTransaction( return nil, fmt.Errorf("moved funds UTXO is required") } + if walletMainUtxo != nil { + scriptType, err := walletMainUtxoScriptType( + bitcoinChain, + walletMainUtxo, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot inspect wallet main UTXO script: [%v]", + err, + ) + } + + if scriptType == bitcoin.P2TRScript { + return nil, fmt.Errorf( + "Taproot moved-funds sweep main UTXOs are not supported " + + "until moved-funds sweep transactions support P2TR " + + "wallet outputs", + ) + } + } + builder := bitcoin.NewTransactionBuilder(bitcoinChain) // The moved funds UTXO is always the first input. diff --git a/pkg/tbtc/moved_funds_sweep_test.go b/pkg/tbtc/moved_funds_sweep_test.go index 68ae7be032..b38d26f23d 100644 --- a/pkg/tbtc/moved_funds_sweep_test.go +++ b/pkg/tbtc/moved_funds_sweep_test.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "math/big" + "strings" "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 +78,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 @@ -211,3 +210,41 @@ func TestAssembleMovedFundsSweepTransaction(t *testing.T) { }) } } + +func TestAssembleMovedFundsSweepTransaction_RejectsTaprootWalletMainUtxo( + t *testing.T, +) { + bitcoinChain := newLocalBitcoinChain() + walletPublicKey := testWalletPublicKeyFromXOnly( + t, + "2336f65004d8f122f1fe947ebd009a8b4add3a0d937356d568e30f7fcc2e4008", + ) + walletMainUtxo := testTaprootWalletMainUtxo( + t, + bitcoinChain, + walletPublicKey, + ) + + var movedFundsTxHash bitcoin.Hash + movedFundsTxHash[0] = 0x02 + + _, err := assembleMovedFundsSweepTransaction( + bitcoinChain, + walletPublicKey, + &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: movedFundsTxHash, + OutputIndex: 0, + }, + Value: 100000, + }, + walletMainUtxo, + 1000, + ) + if err == nil { + t.Fatal("expected Taproot moved-funds sweep main UTXO rejection") + } + if !strings.Contains(err.Error(), "Taproot moved-funds sweep main UTXOs") { + t.Fatalf("unexpected error: [%v]", err) + } +} diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index 1e9c01b0a3..c926aae66b 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -133,8 +133,8 @@ func (mfa *movingFundsAction) execute() error { walletPublicKeyHash := bitcoin.PublicKeyHash(mfa.wallet().publicKey) - walletMainUtxo, err := DetermineWalletMainUtxo( - walletPublicKeyHash, + walletMainUtxo, err := DetermineWalletMainUtxoForPublicKey( + mfa.wallet().publicKey, mfa.chain, mfa.btcChain, ) @@ -151,6 +151,14 @@ func (mfa *movingFundsAction) execute() error { return fmt.Errorf("moving funds wallet has no main UTXO") } + err = ensureMovingFundsMainUtxoSupportsLegacyTargets( + mfa.btcChain, + walletMainUtxo, + ) + if err != nil { + return fmt.Errorf("unsupported moving funds wallet main UTXO: [%v]", err) + } + // Perform initial validation of the moving funds proposal. err = ValidateMovingFundsProposal( validateProposalLogger, @@ -188,8 +196,8 @@ func (mfa *movingFundsAction) execute() error { return fmt.Errorf("validate proposal step failed: [%v]", err) } - err = EnsureWalletSyncedBetweenChains( - walletPublicKeyHash, + err = EnsureWalletSyncedBetweenChainsForPublicKey( + mfa.wallet().publicKey, walletMainUtxo, mfa.chain, mfa.btcChain, @@ -574,8 +582,16 @@ func assembleMovingFundsTransaction( return nil, fmt.Errorf("wallet main UTXO is required") } + err := ensureMovingFundsMainUtxoSupportsLegacyTargets( + bitcoinChain, + walletMainUtxo, + ) + if err != nil { + return nil, err + } + builder := bitcoin.NewTransactionBuilder(bitcoinChain) - err := builder.AddPublicKeyHashInput(walletMainUtxo) + err = builder.AddPublicKeyHashInput(walletMainUtxo) if err != nil { return nil, fmt.Errorf( "cannot add input pointing to wallet main UTXO: [%v]", @@ -627,3 +643,22 @@ func assembleMovingFundsTransaction( return builder, nil } + +func ensureMovingFundsMainUtxoSupportsLegacyTargets( + bitcoinChain bitcoin.Chain, + walletMainUtxo *bitcoin.UnspentTransactionOutput, +) error { + scriptType, err := walletMainUtxoScriptType(bitcoinChain, walletMainUtxo) + if err != nil { + return err + } + + if scriptType == bitcoin.P2TRScript { + return fmt.Errorf( + "Taproot moving-funds main UTXOs are not supported until " + + "moving-funds transactions support P2TR target wallet outputs", + ) + } + + return nil +} diff --git a/pkg/tbtc/moving_funds_test.go b/pkg/tbtc/moving_funds_test.go index d1fb2b99d4..5609efc820 100644 --- a/pkg/tbtc/moving_funds_test.go +++ b/pkg/tbtc/moving_funds_test.go @@ -6,13 +6,14 @@ import ( "fmt" "math/big" "reflect" + "strings" "testing" "time" "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 +93,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 +108,7 @@ func TestMovingFundsAction_Execute(t *testing.T) { signingExecutor.setSignatures( []*big.Int{scenario.ExpectedSigHash}, proposalProcessingStartBlock+movingFundsCommitmentConfirmationBlocks, - []*tecdsa.Signature{rawSignature}, + []*frost.Signature{rawSignature}, ) action := newMovingFundsAction( @@ -227,6 +227,63 @@ func TestAssembleMovingFundsTransaction(t *testing.T) { } } +func TestAssembleMovingFundsTransaction_RejectsTaprootWalletMainUtxo( + t *testing.T, +) { + var taprootOutputKey [32]byte + taprootOutputKey[31] = 1 + + taprootScript, err := bitcoin.PayToTaproot(taprootOutputKey) + if err != nil { + t.Fatal(err) + } + + fundingTx := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{}, + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 100000, + PublicKeyScript: taprootScript, + }, + }, + } + + bitcoinChain := newLocalBitcoinChain() + if err := bitcoinChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatal(err) + } + + _, err = assembleMovingFundsTransaction( + bitcoinChain, + &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTx.Hash(), + OutputIndex: 0, + }, + Value: 100000, + }, + [][20]byte{ + hexToByte20("c7302d75072d78be94eb8d36c4b77583c7abb06e"), + }, + 1000, + ) + if err == nil { + t.Fatal("expected Taproot moving-funds main UTXO rejection") + } + if !strings.Contains(err.Error(), "Taproot moving-funds main UTXOs") { + t.Fatalf("unexpected error: [%v]", err) + } +} + func TestValidateMovingFundsSafetyMargin(t *testing.T) { walletPublicKeyHash := hexToByte20( "ffb3f7538bfa98a511495dd96027cfbd57baf2fa", 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..30658c0715 --- /dev/null +++ b/pkg/tbtc/native_tbtc_signer_build_taproot_tx_frost_native_tbtc_signer.go @@ -0,0 +1,108 @@ +//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 for the observational BuildTaprootTx + // phase when native bridge support is unavailable. + 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 { + // 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"` + }{ + 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/node.go b/pkg/tbtc/node.go index f8f40b9f7c..ff605a1d8b 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 ( @@ -136,6 +136,17 @@ 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) + } + walletRegistry, err := newWalletRegistry( keyStorePersistance, chain.CalculateWalletID, @@ -177,25 +188,31 @@ func newNode( return nil, fmt.Errorf("cannot get node's operator address: [%v]", err) } - // TODO: This chicken and egg problem should be solved when - // waitForBlockHeight becomes a part of BlockHeightWaiter interface. - node.dkgExecutor = newDkgExecutor( - node.groupParameters, - node.operatorID, - operatorAddress, - chain, - netProvider, - walletRegistry, - latch, - config, - workPersistence, - scheduler, - node.waitForBlockHeight, - ) + if shouldRunLegacyECDSA(config) { + // TODO: This chicken and egg problem should be solved when + // waitForBlockHeight becomes a part of BlockHeightWaiter interface. + node.dkgExecutor = newDkgExecutor( + node.groupParameters, + node.operatorID, + operatorAddress, + chain, + netProvider, + walletRegistry, + latch, + config, + workPersistence, + scheduler, + node.waitForBlockHeight, + ) + } 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 { @@ -205,6 +222,25 @@ func (n *node) setPerformanceMetrics(metrics interface { }) { 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, + ) + } + } + // Initialize window metrics tracker with performance metrics // Keep metrics for the last 100 windows (approximately 25 hours at 900 blocks per window) if perfMetrics, ok := metrics.(clientinfo.PerformanceMetricsRecorder); ok { @@ -295,6 +331,11 @@ func (n *node) joinDKGIfEligible( startBlock uint64, delayBlocks uint64, ) { + if n.dkgExecutor == nil { + logger.Warnf("legacy ECDSA DKG is disabled; ignoring DKG started event") + return + } + n.dkgExecutor.executeDkgIfEligible(seed, startBlock, delayBlocks) } @@ -309,6 +350,11 @@ func (n *node) validateDKG( result *DKGChainResult, resultHash [32]byte, ) { + if n.dkgExecutor == nil { + logger.Warnf("legacy ECDSA DKG is disabled; ignoring DKG result") + return + } + n.dkgExecutor.executeDkgValidation(seed, submissionBlock, result, resultHash) } @@ -1278,27 +1324,59 @@ func (n *node) archiveClosedWallets() error { for _, walletPublicKey := range walletPublicKeys { walletPublicKeyHash := bitcoin.PublicKeyHash(walletPublicKey) - walletID, err := n.chain.CalculateWalletID(walletPublicKey) - if err != nil { - return fmt.Errorf( - "could not calculate wallet ID for wallet with public key "+ - "hash [0x%x]: [%v]", - walletPublicKeyHash, - err, - ) - } + var walletID [32]byte + var archiveWallet bool - isRegistered, err := n.chain.IsWalletRegistered(walletID) + walletChainData, err := n.chain.GetWallet(walletPublicKeyHash) if err != nil { - return fmt.Errorf( - "could not check if wallet is registered for wallet with ID "+ - "[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 Bridge wallet state is + // unavailable. FROST wallets are registered in the Bridge but not + // in the legacy ECDSA wallet registry. + isRegistered, err := n.chain.IsWalletRegistered(walletID) + if err != nil { + return fmt.Errorf( + "could not check if wallet is registered for wallet with ECDSA ID "+ + "[0x%x]: [%v]", + walletID, + err, + ) + } + + if !isRegistered && n.frostWalletRegistryAvailable() { + logger.Infof( + "wallet with ECDSA ID [0x%x] and public key hash [0x%x] "+ + "was not found in Bridge or the legacy ECDSA registry; "+ + "preserving local key material because FROST wallet "+ + "registration is available and the wallet may be "+ + "pending Bridge registration", + walletID, + walletPublicKeyHash, + ) + continue + } + + archiveWallet = !isRegistered + } else { + walletID = walletChainData.WalletID + if walletID == [32]byte{} { + walletID = DeriveLegacyWalletID(walletPublicKeyHash) + } + + archiveWallet = walletChainData.State == StateClosed || + walletChainData.State == StateTerminated } - if !isRegistered { + if archiveWallet { // If the wallet is no longer registered it means the wallet has // been closed or terminated. err := n.walletRegistry.archiveWallet(walletPublicKeyHash) @@ -1322,6 +1400,16 @@ func (n *node) archiveClosedWallets() error { return nil } +type frostWalletRegistryAvailability interface { + FrostWalletRegistryAvailable() bool +} + +func (n *node) frostWalletRegistryAvailable() bool { + frostChain, ok := n.chain.(frostWalletRegistryAvailability) + + return ok && frostChain.FrostWalletRegistryAvailable() +} + // handleWalletClosure handles the wallet termination or closing process. func (n *node) handleWalletClosure(walletID [32]byte) error { blockCounter, err := n.chain.BlockCounter() @@ -1365,20 +1453,47 @@ 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 { + // 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, + 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) @@ -1429,6 +1544,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() { diff --git a/pkg/tbtc/node_signing_backend_test.go b/pkg/tbtc/node_signing_backend_test.go new file mode 100644 index 0000000000..b652dad140 --- /dev/null +++ b/pkg/tbtc/node_signing_backend_test.go @@ -0,0 +1,136 @@ +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 { + 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() + frostsigning.UnregisterNativeExecutionAdapter() + t.Cleanup(frostsigning.ResetExecutionBackend) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + + 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, + ) + } +} + +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() + 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(), + ) + } +} + +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 new file mode 100644 index 0000000000..4162814113 --- /dev/null +++ b/pkg/tbtc/node_startup_signing_backend_test.go @@ -0,0 +1,200 @@ +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_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() + 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 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, +) ( + *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) +} diff --git a/pkg/tbtc/node_test.go b/pkg/tbtc/node_test.go index bedfb30995..ae18a37c70 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", @@ -287,6 +285,138 @@ func TestNode_GetCoordinationExecutor(t *testing.T) { } } +func TestNode_KeepsLiveBridgeWalletWithoutLegacyRegistration(t *testing.T) { + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + localChain := Connect() + localProvider := local.Connect() + + signer := createMockSigner(t) + walletPublicKeyHash := bitcoin.PublicKeyHash(signer.wallet.publicKey) + + localChain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + WalletID: [32]byte{31: 0x01}, + State: StateLive, + }, + ) + + n, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + localProvider, + createMockKeyStorePersistence(t, signer), + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{}, + ) + if err != nil { + t.Fatal(err) + } + + _, ok := n.walletRegistry.getWalletByPublicKeyHash(walletPublicKeyHash) + if !ok { + t.Fatal("live Bridge wallet should not be archived") + } +} + +func TestNode_KeepsPendingFrostWalletWithoutBridgeRegistration(t *testing.T) { + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + localChain := Connect() + localChain.frostWalletRegistryAvailable = true + localProvider := local.Connect() + + signer := createMockSigner(t) + walletPublicKeyHash := bitcoin.PublicKeyHash(signer.wallet.publicKey) + + n, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + localProvider, + createMockKeyStorePersistence(t, signer), + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{}, + ) + if err != nil { + t.Fatal(err) + } + + _, ok := n.walletRegistry.getWalletByPublicKeyHash(walletPublicKeyHash) + if !ok { + t.Fatal("pending FROST wallet should not be archived") + } +} + +func TestNode_ArchivesClosedBridgeWallet(t *testing.T) { + testCases := map[string]WalletState{ + "closed": StateClosed, + "terminated": StateTerminated, + } + + for name, walletState := range testCases { + t.Run(name, func(t *testing.T) { + groupParameters := &GroupParameters{ + GroupSize: 5, + GroupQuorum: 4, + HonestThreshold: 3, + } + + localChain := Connect() + localProvider := local.Connect() + + signer := createMockSigner(t) + walletPublicKeyHash := bitcoin.PublicKeyHash( + signer.wallet.publicKey, + ) + + localChain.setWallet( + walletPublicKeyHash, + &WalletChainData{ + WalletID: [32]byte{31: 0x01}, + State: walletState, + }, + ) + + n, err := newNode( + groupParameters, + localChain, + newLocalBitcoinChain(), + localProvider, + createMockKeyStorePersistence(t, signer), + &mockPersistenceHandle{}, + generator.StartScheduler(), + &mockCoordinationProposalGenerator{}, + Config{}, + ) + if err != nil { + t.Fatal(err) + } + + _, ok := n.walletRegistry.getWalletByPublicKeyHash( + walletPublicKeyHash, + ) + if ok { + t.Fatal("closed Bridge wallet should be archived") + } + }) + } +} + func TestNode_RunCoordinationLayer(t *testing.T) { groupParameters := &GroupParameters{ GroupSize: 5, @@ -491,6 +621,7 @@ func createMockSigner(t *testing.T) *signer { }, signingGroupMemberIndex: group.MemberIndex(1), privateKeyShare: privateKeyShare, + signerMaterial: privateKeyShare, } } diff --git a/pkg/tbtc/redemption.go b/pkg/tbtc/redemption.go index 1dd950c95f..53064f887a 100644 --- a/pkg/tbtc/redemption.go +++ b/pkg/tbtc/redemption.go @@ -191,8 +191,8 @@ func (ra *redemptionAction) execute() error { return fmt.Errorf("validate proposal step failed: [%v]", err) } - walletMainUtxo, err := DetermineWalletMainUtxo( - walletPublicKeyHash, + walletMainUtxo, err := DetermineWalletMainUtxoForPublicKey( + ra.wallet().publicKey, ra.chain, ra.btcChain, ) @@ -215,8 +215,8 @@ func (ra *redemptionAction) execute() error { return fmt.Errorf("redeeming wallet has no main UTXO") } - err = EnsureWalletSyncedBetweenChains( - walletPublicKeyHash, + err = EnsureWalletSyncedBetweenChainsForPublicKey( + ra.wallet().publicKey, walletMainUtxo, ra.chain, ra.btcChain, @@ -508,14 +508,31 @@ func assembleRedemptionTransaction( // If we can have a non-zero change, construct it. if changeOutputValue > 0 { - changeOutputScript, err := bitcoin.PayToWitnessPublicKeyHash( - bitcoin.PublicKeyHash(walletPublicKey), - ) - if err != nil { - return nil, fmt.Errorf( - "cannot compute change output script: [%v]", - err, + var changeOutputScript bitcoin.Script + var err error + if builder.HasOnlyTaprootKeyPathInputs() { + walletXOnlyPublicKey, err := walletXOnlyPublicKey(walletPublicKey) + if err != nil { + return nil, err + } + + changeOutputScript, err = bitcoin.PayToTaproot(walletXOnlyPublicKey) + if err != nil { + return nil, fmt.Errorf( + "cannot compute Taproot change output script: [%v]", + err, + ) + } + } else { + changeOutputScript, err = bitcoin.PayToWitnessPublicKeyHash( + bitcoin.PublicKeyHash(walletPublicKey), ) + if err != nil { + return nil, fmt.Errorf( + "cannot compute change output script: [%v]", + err, + ) + } } changeOutput := &bitcoin.TransactionOutput{ 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/registry.go b/pkg/tbtc/registry.go index d39b56849d..69a29e36fc 100644 --- a/pkg/tbtc/registry.go +++ b/pkg/tbtc/registry.go @@ -67,7 +67,10 @@ func newWalletRegistry( // them. wallet := signers[0].wallet walletPublicKeyHash := bitcoin.PublicKeyHash(wallet.publicKey) - walletID, err := calculateWalletIdFunc(wallet.publicKey) + walletID, err := calculateWalletIDForSigner( + signers[0], + calculateWalletIdFunc, + ) if err != nil { return nil, fmt.Errorf( "error while calculating wallet ID for wallet with public "+ @@ -134,7 +137,10 @@ func (wr *walletRegistry) registerSigner(signer *signer) error { // the hashes are computed only once. No need to initialize signers slice as // appending works with nil values. if _, ok := wr.walletCache[walletStorageKey]; !ok { - walletID, err := wr.calculateWalletIdFunc(signer.wallet.publicKey) + walletID, err := calculateWalletIDForSigner( + signer, + wr.calculateWalletIdFunc, + ) if err != nil { return fmt.Errorf("cannot calculate wallet ID: [%v]", err) } 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/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/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/signer_material_encoding.go b/pkg/tbtc/signer_material_encoding.go new file mode 100644 index 0000000000..dba6e6e7f4 --- /dev/null +++ b/pkg/tbtc/signer_material_encoding.go @@ -0,0 +1,300 @@ +package tbtc + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "encoding/json" + "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:") + +// signerMaterialMaxFormatLength bounds the length of the format identifier in +// a serialized signer-material envelope. Real format identifiers are short +// labels like "frost-tbtc-signer-v1", so 256 bytes is generous; the cap exists +// to refuse a uvarint-claimed length that would allocate a huge string from a +// hostile or corrupted payload before the existing `offset+int(formatLength) > +// len(data)` bounds check runs. +const signerMaterialMaxFormatLength uint64 = 256 + +// signerMaterialMaxPayloadLength bounds the length of the payload body. JSON +// envelopes for FROST and the tBTC-signer key material carry tens of KiB of +// hex; 256 KiB is comfortably above that and refuses a uvarint-claimed length +// that would allocate hundreds of MiB from a corrupted state file or a +// hostile peer. +const signerMaterialMaxPayloadLength uint64 = 256 * 1024 + +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: + // Transitional compatibility: raw bytes are treated as legacy + // frost-uniffi-v1 payloads from previously persisted signer entries. + 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 { + privateKeyShare := legacyPrivateKeyShareFromNativeSignerMaterial( + nativeSignerMaterial, + ) + + return &unmarshaledSignerMaterial{ + signerMaterial: nativeSignerMaterial, + privateKeyShare: privateKeyShare, + }, nil + } + + privateKeyShare := &tecdsa.PrivateKeyShare{} + if err := privateKeyShare.Unmarshal(data); err != nil { + 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: resolvedSignerMaterial, + 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) + } + if formatLength > signerMaterialMaxFormatLength { + return nil, true, fmt.Errorf( + "signer material format length %d exceeds maximum %d", + formatLength, + signerMaterialMaxFormatLength, + ) + } + 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) + } + if payloadLength > signerMaterialMaxPayloadLength { + return nil, true, fmt.Errorf( + "signer material payload length %d exceeds maximum %d", + payloadLength, + signerMaterialMaxPayloadLength, + ) + } + 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 +} + +func legacyPrivateKeyShareFromNativeSignerMaterial( + nativeSignerMaterial *frostsigning.NativeSignerMaterial, +) *tecdsa.PrivateKeyShare { + if nativeSignerMaterial == nil { + return nil + } + + switch nativeSignerMaterial.Format { + case frostsigning.NativeSignerMaterialFormatFrostUniFFIV1: + privateKeyShare := &tecdsa.PrivateKeyShare{} + if err := privateKeyShare.Unmarshal(nativeSignerMaterial.Payload); err != nil { + return nil + } + + return privateKeyShare + + case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: + var payload frostsigning.NativeTBTCSignerMaterialPayload + 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 + } +} 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_frost_native_test.go b/pkg/tbtc/signer_material_encoding_frost_native_test.go new file mode 100644 index 0000000000..e6bcbd8caf --- /dev/null +++ b/pkg/tbtc/signer_material_encoding_frost_native_test.go @@ -0,0 +1,155 @@ +//go:build frost_native + +package tbtc + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "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( + 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.NativeSignerMaterialFormatFrostTBTCSignerV1 { + t.Fatalf( + "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]", + legacyEncoded, + actualPayload, + ) + } +} + +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") + } +} diff --git a/pkg/tbtc/signer_material_encoding_test.go b/pkg/tbtc/signer_material_encoding_test.go new file mode 100644 index 0000000000..fdef4ccb34 --- /dev/null +++ b/pkg/tbtc/signer_material_encoding_test.go @@ -0,0 +1,335 @@ +package tbtc + +import ( + "bytes" + "encoding/binary" + "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" +) + +// appendUvarintForTest emits a uvarint matching the format +// `unmarshalSignerMaterialFromPersistence` expects. It is duplicated in the +// test package rather than exported so test-only construction of corrupted +// envelopes cannot accidentally be reused by production code. +func appendUvarintForTest(buf []byte, value uint64) []byte { + var scratch [binary.MaxVarintLen64]byte + n := binary.PutUvarint(scratch[:], value) + return append(buf, scratch[:n]...) +} + +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) { + 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, + payload, + ) + 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 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) + 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, + ) + } + + 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) { + 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 TestUnmarshalSignerMaterialFromPersistence_RejectsOversizedFormatLength( + t *testing.T, +) { + // Build an envelope that claims a format length one byte above the cap. + // The body itself is short, so without the length cap the bounds check + // would still catch this, but the cap rejects the claim earlier and with + // a clear error before any allocation. + encoded := append([]byte{}, signerMaterialEnvelopePrefix...) + encoded = appendUvarintForTest(encoded, signerMaterialMaxFormatLength+1) + encoded = append(encoded, []byte("ignored")...) + + _, err := unmarshalSignerMaterialFromPersistence(encoded) + if err == nil { + t.Fatal("expected unmarshal error") + } + + if !strings.Contains(err.Error(), "format length") || + !strings.Contains(err.Error(), "exceeds maximum") { + t.Fatalf( + "unexpected unmarshal error\nexpected substrings: [format length], [exceeds maximum]\nactual: [%v]", + err, + ) + } +} + +func TestUnmarshalSignerMaterialFromPersistence_RejectsOversizedPayloadLength( + t *testing.T, +) { + encoded := append([]byte{}, signerMaterialEnvelopePrefix...) + format := []byte(frostsigning.NativeSignerMaterialFormatFrostUniFFIV1) + encoded = appendUvarintForTest(encoded, uint64(len(format))) + encoded = append(encoded, format...) + encoded = appendUvarintForTest(encoded, signerMaterialMaxPayloadLength+1) + + _, err := unmarshalSignerMaterialFromPersistence(encoded) + if err == nil { + t.Fatal("expected unmarshal error") + } + + if !strings.Contains(err.Error(), "payload length") || + !strings.Contains(err.Error(), "exceeds maximum") { + t.Fatalf( + "unexpected unmarshal error\nexpected substrings: [payload length], [exceeds maximum]\nactual: [%v]", + 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) + } +} diff --git a/pkg/tbtc/signer_material_resolver.go b/pkg/tbtc/signer_material_resolver.go new file mode 100644 index 0000000000..ce9dc08d06 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver.go @@ -0,0 +1,109 @@ +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) +} + +// 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( + 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{} + signerMaterialResolverProviderForBuild SignerMaterialResolverProviderForBuild +) + +// 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{} +} + +// 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() + + return signerMaterialResolver +} + +func currentSignerMaterialResolverProviderForBuild() SignerMaterialResolverProviderForBuild { + signerMaterialResolverMutex.RLock() + defer signerMaterialResolverMutex.RUnlock() + + return signerMaterialResolverProviderForBuild +} + +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_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..3cef396081 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_build_frost_native.go @@ -0,0 +1,74 @@ +//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 native signer +// 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( + 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(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.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + }, nil +} 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..268b53a521 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_tbtc_signer.go @@ -0,0 +1,91 @@ +//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) + + // Scaffold-era key-group derivation: the current value identifies + // placeholder material derived from the legacy wallet public-key hash, + // not the output of a real FROST DKG run. Refuse to surface that material + // at all unless the operator has explicitly opted in via + // AcceptScaffoldKeyGroupEnvVar — production deployments must never set + // this. See native_tbtc_signer_material.go for the env-var contract. + if !frostsigning.AcceptScaffoldKeyGroupEnabled() { + return nil, fmt.Errorf( + "refusing to build scaffold-era %q signer material; set %s=true to "+ + "opt in for local/CI use only, never in production", + frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + frostsigning.AcceptScaffoldKeyGroupEnvVar, + ) + } + + // 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: 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.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 new file mode 100644 index 0000000000..23f4b36b8b --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_build_frost_native_test.go @@ -0,0 +1,242 @@ +//go:build frost_native + +package tbtc + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "strings" + "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, +) { + // Default scaffold-era resolver builds legacy-wallet-pubkey signer + // material; production refuses it but local/CI tests can opt in. + t.Setenv(frostsigning.AcceptScaffoldKeyGroupEnvVar, "true") + + 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, + ) + } + + expectedPayload, err := privateKeyShare.Marshal() + if err != nil { + t.Fatalf("failed marshaling expected private key share: [%v]", err) + } + + if nativeSignerMaterial.Format != frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1 { + t.Fatalf( + "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]", + expectedPayload, + actualPayload, + ) + } +} + +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") + } +} + +func TestRegisterSignerMaterialResolverForBuild_DefaultProviderRefusesScaffoldWithoutOptIn( + t *testing.T, +) { + // Force the env var to "" so a stray external value cannot suppress the + // scaffold refusal during this regression test. + t.Setenv(frostsigning.AcceptScaffoldKeyGroupEnvVar, "") + + 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 + _, err = resolveSignerMaterial(privateKeyShare) + if err == nil { + t.Fatal( + "expected scaffold-refusal error from default resolver without opt-in", + ) + } + + if !strings.Contains(err.Error(), frostsigning.AcceptScaffoldKeyGroupEnvVar) { + t.Fatalf( + "expected scaffold-refusal error to reference %s; got: [%v]", + frostsigning.AcceptScaffoldKeyGroupEnvVar, + err, + ) + } + if !strings.Contains(err.Error(), frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey) { + t.Fatalf( + "expected scaffold-refusal error to reference %s; got: [%v]", + frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + err, + ) + } +} 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 new file mode 100644 index 0000000000..52ef802800 --- /dev/null +++ b/pkg/tbtc/signer_material_resolver_test.go @@ -0,0 +1,129 @@ +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 TestRegisterSignerMaterialResolverProviderForBuild_Nil(t *testing.T) { + err := RegisterSignerMaterialResolverProviderForBuild(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/signing.go b/pkg/tbtc/signing.go index 346b6b0446..d85e776916 100644 --- a/pkg/tbtc/signing.go +++ b/pkg/tbtc/signing.go @@ -2,6 +2,8 @@ package tbtc import ( "context" + "crypto/sha256" + "encoding/binary" "fmt" "math/big" "strings" @@ -9,12 +11,13 @@ 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/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" "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" ) @@ -40,6 +43,29 @@ const ( // cannot execute the requested signature due to an ongoing signing. var errSigningExecutorBusy = fmt.Errorf("signing executor is busy") +func signingSessionID( + message *big.Int, + taprootMerkleRoot *[32]byte, + startBlock uint64, + attemptNumber uint, +) string { + if taprootMerkleRoot == nil { + return fmt.Sprintf("%v-%v", message.Text(16), attemptNumber) + } + + var startBlockBytes [8]byte + binary.BigEndian.PutUint64(startBlockBytes[:], startBlock) + + sessionDigest := sha256.New() + sessionDigest.Write([]byte(message.Text(16))) + sessionDigest.Write([]byte{0}) + sessionDigest.Write(taprootMerkleRoot[:]) + sessionDigest.Write([]byte{0}) + sessionDigest.Write(startBlockBytes[:]) + + return fmt.Sprintf("tr-%x-%v", sessionDigest.Sum(nil), attemptNumber) +} + // signingExecutor is a component responsible for executing signing related to // a specific wallet whose part is controlled by this node. type signingExecutor struct { @@ -69,6 +95,8 @@ type signingExecutor struct { } } +var _ schnorrWalletSigningExecutor = (*signingExecutor)(nil) + func newSigningExecutor( signers []*signer, broadcastChannel net.BroadcastChannel, @@ -92,6 +120,16 @@ func newSigningExecutor( } } +func (se *signingExecutor) usesSchnorrSignatures() bool { + for _, signer := range se.signers { + if signingMaterialUsesSchnorrSignatures(signer.signingMaterial()) { + return true + } + } + + return false +} + // signBatch performs the signing process for each message from the given // messages batch, one after another. If at least one message cannot be signed, // this function returns an error. If all messages were signed successfully, @@ -102,7 +140,24 @@ func (se *signingExecutor) signBatch( ctx context.Context, messages []*big.Int, startBlock uint64, -) ([]*tecdsa.Signature, error) { +) ([]*frost.Signature, error) { + return se.signBatchWithTaprootMerkleRoots(ctx, messages, nil, startBlock) +} + +func (se *signingExecutor) signBatchWithTaprootMerkleRoots( + ctx context.Context, + messages []*big.Int, + taprootMerkleRoots []*[32]byte, + startBlock uint64, +) ([]*frost.Signature, error) { + if taprootMerkleRoots != nil && len(taprootMerkleRoots) != len(messages) { + return nil, fmt.Errorf( + "taproot merkle roots count [%v] does not match messages count [%v]", + len(taprootMerkleRoots), + len(messages), + ) + } + wallet := se.wallet() walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) @@ -139,7 +194,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 { @@ -154,7 +209,17 @@ func (se *signingExecutor) signBatch( signingStartBlock = endBlocks[i-1] + signingBatchInterludeBlocks } - signature, _, endBlock, err := se.sign(ctx, message, signingStartBlock) + var taprootMerkleRoot *[32]byte + if taprootMerkleRoots != nil { + taprootMerkleRoot = taprootMerkleRoots[i] + } + + signature, _, endBlock, err := se.signWithTaprootMerkleRoot( + ctx, + message, + taprootMerkleRoot, + signingStartBlock, + ) if err != nil { // Error metrics are recorded in the sign() method for all error paths. return nil, err @@ -184,7 +249,16 @@ func (se *signingExecutor) sign( ctx context.Context, message *big.Int, startBlock uint64, -) (*tecdsa.Signature, *signingActivityReport, uint64, error) { +) (*frost.Signature, *signingActivityReport, uint64, error) { + return se.signWithTaprootMerkleRoot(ctx, message, nil, startBlock) +} + +func (se *signingExecutor) signWithTaprootMerkleRoot( + ctx context.Context, + message *big.Int, + taprootMerkleRoot *[32]byte, + startBlock uint64, +) (*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 +297,7 @@ func (se *signingExecutor) sign( ) type signingOutcome struct { - signature *tecdsa.Signature + signature *frost.Signature activityReport *signingActivityReport endBlock uint64 } @@ -291,12 +365,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. @@ -313,26 +413,31 @@ func (se *signingExecutor) sign( se.waitForBlockFn, ) - sessionID := fmt.Sprintf( - "%v-%v", - message.Text(16), + sessionID := signingSessionID( + message, + taprootMerkleRoot, + startBlock, 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, - ), - attempt.excludedMembersIndexes, - se.broadcastChannel, - se.membershipValidator, + &signing.Request{ + Message: message, + SessionID: sessionID, + MemberIndex: signer.signingGroupMemberIndex, + SignerMaterial: signer.signingMaterial(), + PrivateKeyShare: signer.privateKeyShare, + TaprootMerkleRoot: taprootMerkleRoot, + GroupSize: wallet.groupSize(), + DishonestThreshold: wallet.groupDishonestThreshold( + se.groupParameters.HonestThreshold, + ), + Channel: se.broadcastChannel, + MembershipValidator: se.membershipValidator, + Attempt: attemptInfo, + }, ) if err != nil { return nil, 0, err @@ -437,6 +542,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_done.go b/pkg/tbtc/signing_done.go index 58dfeccc83..f14426d87f 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 } @@ -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) { - var signature *tecdsa.Signature + 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 +} 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..367274ce41 100644 --- a/pkg/tbtc/signing_loop.go +++ b/pkg/tbtc/signing_loop.go @@ -13,9 +13,8 @@ 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" ) @@ -45,6 +44,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 { @@ -96,6 +106,12 @@ type signingRetryLoop struct { attemptSeed int64 doneCheck signingDoneCheckStrategy + + // participantSelector dispatches qualified-operator selection. + // Default: legacy retry shuffle. Phase 7 may install a + // ROAST-driven implementation behind the frost_roast_retry + // build tag once AggregateBundle production is wired upstream. + participantSelector signingParticipantSelector } func newSigningRetryLoop( @@ -108,13 +124,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,8 +133,9 @@ func newSigningRetryLoop( announcer: announcer, attemptCounter: 0, attemptStartBlock: initialStartBlock, - attemptSeed: attemptSeed, + attemptSeed: signingAttemptSeed(message), doneCheck: doneCheck, + participantSelector: defaultSigningParticipantSelector(), } } @@ -488,11 +498,16 @@ func (srl *signingRetryLoop) qualifiedOperatorsSet( ) } - qualifiedOperators, err := retry.EvaluateRetryParticipantsForSigning( + // RFC-21 Phase 6.4: dispatch through participantSelector so a + // future ROAST-driven implementation can be installed behind + // the frost_roast_retry build tag without touching this call + // site. Default and current behaviour: legacy retry shuffle. + qualifiedOperators, err := srl.participantSelector.Select( readySigningGroupOperators, srl.attemptSeed, retryCount, uint(srl.groupParameters.HonestThreshold), + fmt.Sprintf("%v", srl.message), ) if err != nil { return nil, fmt.Errorf( diff --git a/pkg/tbtc/signing_loop_legacy_selector.go b/pkg/tbtc/signing_loop_legacy_selector.go new file mode 100644 index 0000000000..f9bc758717 --- /dev/null +++ b/pkg/tbtc/signing_loop_legacy_selector.go @@ -0,0 +1,42 @@ +package tbtc + +import ( + "fmt" + + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost/retry" +) + +// legacySigningParticipantSelector is the pre-RFC-21 implementation: +// it calls the pseudo-random retry shuffle in pkg/frost/retry. +// Kept as the canonical fallback through Phase 6; Phase 7 may +// remove it once the ROAST-driven retry path is fully wired and +// the readiness manifest flips. +// +// The legacy code is *intentionally retained* through Phase 6 to +// preserve the operational rollback path: if a deployment toggles +// the readiness env var off, this implementation is what the +// dispatcher falls back to. +type legacySigningParticipantSelector struct{} + +func (legacySigningParticipantSelector) Select( + members []chain.Address, + seed int64, + retryCount uint, + honestThreshold uint, + _ string, +) ([]chain.Address, error) { + qualifiedOperators, err := retry.EvaluateRetryParticipantsForSigning( + members, + seed, + retryCount, + honestThreshold, + ) + if err != nil { + return nil, fmt.Errorf( + "legacy participant selector: random operator selection failed: %w", + err, + ) + } + return qualifiedOperators, nil +} diff --git a/pkg/tbtc/signing_loop_roast_dispatcher.go b/pkg/tbtc/signing_loop_roast_dispatcher.go new file mode 100644 index 0000000000..d9d4dcb088 --- /dev/null +++ b/pkg/tbtc/signing_loop_roast_dispatcher.go @@ -0,0 +1,43 @@ +package tbtc + +import ( + "github.com/keep-network/keep-core/pkg/chain" +) + +// signingParticipantSelector picks the set of operators qualified for +// a signing attempt. The legacy implementation is the pseudo-random +// retry shuffle in pkg/frost/retry; the RFC-21 Phase-6 migration +// introduces this interface so an alternate ROAST-driven +// implementation can be installed behind the frost_roast_retry build +// tag without touching the call site. +// +// PR 6.4 ships the dispatcher with only the legacy implementation +// installed; Phase 7 wires the ROAST-driven implementation along +// with the supporting AggregateBundle production at the executor- +// adapter layer. Until Phase 7, behaviour is byte-identical to +// pre-RFC-21 retry semantics. +type signingParticipantSelector interface { + // Select returns the set of operators qualified to participate + // in the given signing attempt. members is the set of operators + // whose ready signal was received for this attempt. seed is the + // per-message retry seed; retryCount is 0-based (i.e. 0 for the + // first retry). honestThreshold is the group's signing + // threshold. + Select( + members []chain.Address, + seed int64, + retryCount uint, + honestThreshold uint, + sessionID string, + ) ([]chain.Address, error) +} + +// defaultSigningParticipantSelector returns the build-default +// implementation. Default build: the legacy retry shuffle. Tagged +// build (frost_roast_retry, Phase 7.2): a ROAST-driven selector +// that consults the per-session TransitionMessage registry and +// falls back to the legacy selector when no bundle is available. +// +// Defined in build-tagged sibling files +// (signing_loop_selector_*.go) so the right implementation is +// chosen at compile time without runtime branching. diff --git a/pkg/tbtc/signing_loop_roast_dispatcher_test.go b/pkg/tbtc/signing_loop_roast_dispatcher_test.go new file mode 100644 index 0000000000..3d5aa60f00 --- /dev/null +++ b/pkg/tbtc/signing_loop_roast_dispatcher_test.go @@ -0,0 +1,132 @@ +package tbtc + +import ( + "errors" + "testing" + + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +// Note: TestDefaultSigningParticipantSelector_IsLegacy below is +// build-tag-conditional (see _default_build_test.go); under +// frost_roast_retry the default is the ROAST selector and a +// dedicated test verifies that. + +// recordingSelector counts how often Select was called and returns +// a fixed result. Tests use it to assert the dispatcher routes +// participant selection through the configured selector rather +// than the legacy path. +type recordingSelector struct { + calls int + result []chain.Address + err error +} + +func (r *recordingSelector) Select( + members []chain.Address, + _ int64, + _ uint, + _ uint, + _ string, +) ([]chain.Address, error) { + r.calls++ + if r.err != nil { + return nil, r.err + } + if r.result != nil { + return r.result, nil + } + return members, nil +} + +func TestLegacySigningParticipantSelector_DelegatesToRetryShuffle(t *testing.T) { + members := []chain.Address{ + chain.Address("op-1"), + chain.Address("op-2"), + chain.Address("op-3"), + chain.Address("op-4"), + chain.Address("op-5"), + } + sel := legacySigningParticipantSelector{} + got, err := sel.Select(members, 42, 0, 3, "session-x") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(got) < 3 { + t.Fatalf("expected at least 3 qualified operators, got %d", len(got)) + } +} + +func TestLegacySigningParticipantSelector_PropagatesErrors(t *testing.T) { + sel := legacySigningParticipantSelector{} + _, err := sel.Select( + []chain.Address{chain.Address("op-1")}, + 0, 0, + 99, // honest threshold higher than member count + "session-x", + ) + if err == nil { + t.Fatal("expected error from retry shuffle") + } +} + +func TestSigningRetryLoopUsesDispatcher(t *testing.T) { + sentinel := []chain.Address{ + chain.Address("op-1"), + chain.Address("op-2"), + chain.Address("op-3"), + } + recorder := &recordingSelector{result: sentinel} + + srl := &signingRetryLoop{ + signingGroupOperators: chain.Addresses{ + chain.Address("op-1"), + chain.Address("op-2"), + chain.Address("op-3"), + chain.Address("op-4"), + chain.Address("op-5"), + }, + groupParameters: &GroupParameters{ + HonestThreshold: 3, + }, + attemptCounter: 1, + attemptSeed: 42, + participantSelector: recorder, + } + + set, err := srl.qualifiedOperatorsSet([]group.MemberIndex{1, 2, 3, 4, 5}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if recorder.calls != 1 { + t.Fatalf("expected dispatcher to be called once; got %d", recorder.calls) + } + if len(set) != len(sentinel) { + t.Fatalf( + "expected %d qualified operators (the sentinel), got %d", + len(sentinel), len(set), + ) + } +} + +func TestSigningRetryLoopPropagatesSelectorError(t *testing.T) { + wantErr := errors.New("synthetic selector failure") + srl := &signingRetryLoop{ + signingGroupOperators: chain.Addresses{ + chain.Address("op-1"), + chain.Address("op-2"), + }, + groupParameters: &GroupParameters{HonestThreshold: 2}, + attemptCounter: 1, + attemptSeed: 0, + participantSelector: &recordingSelector{err: wantErr}, + } + _, err := srl.qualifiedOperatorsSet([]group.MemberIndex{1, 2}) + if err == nil { + t.Fatal("expected selector error to propagate") + } + if !errors.Is(err, wantErr) { + t.Fatalf("expected wrapped sentinel; got %v", err) + } +} diff --git a/pkg/tbtc/signing_loop_selector_default_build.go b/pkg/tbtc/signing_loop_selector_default_build.go new file mode 100644 index 0000000000..3eb237e93f --- /dev/null +++ b/pkg/tbtc/signing_loop_selector_default_build.go @@ -0,0 +1,12 @@ +//go:build !frost_roast_retry + +package tbtc + +// defaultSigningParticipantSelector in the default build always +// returns the legacy retry shuffle. The ROAST-driven selector is +// only compiled into the frost_roast_retry build (see +// signing_loop_selector_frost_roast_retry.go) so the default +// production binary contains no ROAST-retry code paths at all. +func defaultSigningParticipantSelector() signingParticipantSelector { + return legacySigningParticipantSelector{} +} diff --git a/pkg/tbtc/signing_loop_selector_default_build_test.go b/pkg/tbtc/signing_loop_selector_default_build_test.go new file mode 100644 index 0000000000..ffb604197c --- /dev/null +++ b/pkg/tbtc/signing_loop_selector_default_build_test.go @@ -0,0 +1,15 @@ +//go:build !frost_roast_retry + +package tbtc + +import "testing" + +func TestDefaultSigningParticipantSelector_IsLegacyInDefaultBuild(t *testing.T) { + sel := defaultSigningParticipantSelector() + if _, ok := sel.(legacySigningParticipantSelector); !ok { + t.Fatalf( + "defaultSigningParticipantSelector in default build must return legacy implementation; got %T", + sel, + ) + } +} diff --git a/pkg/tbtc/signing_loop_selector_frost_roast_retry.go b/pkg/tbtc/signing_loop_selector_frost_roast_retry.go new file mode 100644 index 0000000000..8afe8ee326 --- /dev/null +++ b/pkg/tbtc/signing_loop_selector_frost_roast_retry.go @@ -0,0 +1,127 @@ +//go:build frost_roast_retry + +package tbtc + +import ( + "fmt" + + "github.com/keep-network/keep-core/pkg/chain" + "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/protocol/group" +) + +// roastSigningParticipantSelector consumes the per-session +// TransitionMessage registry populated by Phase 7.1's bundle +// production. When a bundle is available for the session, it +// invokes EvaluateRoastRetryForSigning to compute the next +// attempt's IncludedSet from the verified evidence. When no bundle +// is available -- typically the first attempt of a session, or +// when the elected coordinator has not yet produced a transition +// message for the current message -- it falls back to the legacy +// retry shuffle. +// +// The selector is installed as defaultSigningParticipantSelector +// when the binary is built with the frost_roast_retry tag and the +// operator opts in via KEEP_CORE_FROST_ROAST_RETRY_ENABLED. +type roastSigningParticipantSelector struct { + legacy legacySigningParticipantSelector +} + +// defaultSigningParticipantSelector in the frost_roast_retry build +// returns the ROAST-driven selector. Its Select method internally +// dispatches to the bundle-based path when a TransitionMessage is +// available and falls back to the legacy shuffle otherwise, so a +// node that has not yet produced any bundles is observationally +// identical to a legacy-only deployment. +func defaultSigningParticipantSelector() signingParticipantSelector { + return roastSigningParticipantSelector{} +} + +// Select chooses the next attempt's qualified operators. When a +// TransitionMessage is present for sessionID, the selector calls +// EvaluateRoastRetryForSigning with a per-call closure resolver +// that maps group.MemberIndex to chain.Address using the supplied +// members slice. When no bundle is present, the selector falls +// back to the legacy retry shuffle. +func (s roastSigningParticipantSelector) Select( + members []chain.Address, + seed int64, + retryCount uint, + honestThreshold uint, + sessionID string, +) ([]chain.Address, error) { + bundle, ok := signing.TransitionBundleForSession(sessionID) + if !ok || bundle == nil { + return s.legacy.Select( + members, seed, retryCount, honestThreshold, sessionID, + ) + } + deps, registryOK := signing.RegisteredRoastRetryCoordinator() + if !registryOK || deps.Coordinator == nil { + // Should not happen in practice (the bundle was produced + // by a registered coordinator) but defend against the + // race anyway. + return s.legacy.Select( + members, seed, retryCount, honestThreshold, sessionID, + ) + } + + // Look up the AttemptHandle bound to this session. The handle + // identifies the attempt whose bundle we are now consuming; + // NextAttempt is invoked against it to derive the next + // AttemptContext's IncludedSet. + handle, _, handleOK := signing.CurrentAttemptHandleForSession(sessionID) + if !handleOK { + return s.legacy.Select( + members, seed, retryCount, honestThreshold, sessionID, + ) + } + + resolver := membersResolver(members) + addresses, _, err := roast.EvaluateRoastRetryForSigning[chain.Address]( + deps.Coordinator, + handle, + bundle, + honestThreshold, + nil, // DKG public key is recomputed inside Coordinator.NextAttempt; passing nil is acceptable when the bundle's attempt context carries the seed binding. + resolver, + ) + if err != nil { + // Hard-fail per RFC-21 Phase-6 error taxonomy: + // EvaluateRoastRetryForSigning surfaces + // ErrAttemptInfeasible (session structurally failed) or + // resolver errors. Neither is safe to silently fall back + // to legacy, because honest signers would all observe the + // same outcome from the same verified bundle. Surface to + // the caller so the session can be terminated cleanly. + return nil, fmt.Errorf( + "roast signing participant selector: %w", + err, + ) + } + return addresses, nil +} + +// membersResolver is the per-call closure that maps +// group.MemberIndex to chain.Address using the supplied slice. +// Member indices are 1-based (per the FROST group convention) and +// the address at index 0 of `members` corresponds to member index +// 1. +type membersResolver []chain.Address + +func (m membersResolver) For(member group.MemberIndex) (chain.Address, error) { + if member == 0 { + return chain.Address(""), fmt.Errorf( + "member resolver: zero member index", + ) + } + idx := int(member) - 1 + if idx >= len(m) { + return chain.Address(""), fmt.Errorf( + "member resolver: member index %d exceeds members slice length %d", + member, len(m), + ) + } + return m[idx], nil +} diff --git a/pkg/tbtc/signing_loop_selector_frost_roast_retry_test.go b/pkg/tbtc/signing_loop_selector_frost_roast_retry_test.go new file mode 100644 index 0000000000..c60a057ff7 --- /dev/null +++ b/pkg/tbtc/signing_loop_selector_frost_roast_retry_test.go @@ -0,0 +1,219 @@ +//go:build frost_roast_retry + +package tbtc + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/chain" + "github.com/keep-network/keep-core/pkg/frost/roast" + "github.com/keep-network/keep-core/pkg/frost/roast/attempt" + "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestDefaultSigningParticipantSelector_IsROASTInTaggedBuild(t *testing.T) { + sel := defaultSigningParticipantSelector() + if _, ok := sel.(roastSigningParticipantSelector); !ok { + t.Fatalf( + "defaultSigningParticipantSelector in frost_roast_retry build must return ROAST impl; got %T", + sel, + ) + } +} + +func TestROASTSelector_FallsBackToLegacyWhenNoBundle(t *testing.T) { + signing.ResetTransitionBundleRegistryForTest() + t.Cleanup(signing.ResetTransitionBundleRegistryForTest) + + sel := roastSigningParticipantSelector{} + members := []chain.Address{ + chain.Address("op-1"), + chain.Address("op-2"), + chain.Address("op-3"), + chain.Address("op-4"), + chain.Address("op-5"), + } + got, err := sel.Select(members, 42, 0, 3, "session-no-bundle") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(got) < 3 { + t.Fatalf("expected at least 3 from legacy fallback; got %d", len(got)) + } +} + +func TestROASTSelector_FallsBackToLegacyWhenRegistryEmpty(t *testing.T) { + signing.ResetTransitionBundleRegistryForTest() + signing.ResetRoastRetryRegistrationForTest() + signing.ResetSessionHandleRegistryForTest() + t.Cleanup(signing.ResetTransitionBundleRegistryForTest) + t.Cleanup(signing.ResetRoastRetryRegistrationForTest) + t.Cleanup(signing.ResetSessionHandleRegistryForTest) + + // Record a bundle but do NOT register a coordinator. + signing.RecordTransitionBundleForSession( + "session-no-registry", + &roast.TransitionMessage{CoordinatorIDValue: 1}, + ) + + sel := roastSigningParticipantSelector{} + members := []chain.Address{ + chain.Address("op-1"), + chain.Address("op-2"), + chain.Address("op-3"), + chain.Address("op-4"), + chain.Address("op-5"), + } + got, err := sel.Select(members, 42, 0, 3, "session-no-registry") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(got) < 3 { + t.Fatalf("expected at least 3 from legacy fallback; got %d", len(got)) + } +} + +func TestROASTSelector_FallsBackToLegacyWhenNoHandleBinding(t *testing.T) { + signing.ResetTransitionBundleRegistryForTest() + signing.ResetRoastRetryRegistrationForTest() + signing.ResetSessionHandleRegistryForTest() + t.Cleanup(signing.ResetTransitionBundleRegistryForTest) + t.Cleanup(signing.ResetRoastRetryRegistrationForTest) + t.Cleanup(signing.ResetSessionHandleRegistryForTest) + + // Register coordinator + record bundle, but DO NOT bind a + // session handle. The selector must still fall back to legacy + // because it cannot identify which attempt to consume the + // bundle against. + signing.RegisterRoastRetryCoordinator(signing.RoastRetryDeps{ + Coordinator: roast.NewInMemoryCoordinator(), + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: 1, + }) + signing.RecordTransitionBundleForSession( + "session-no-handle", + &roast.TransitionMessage{CoordinatorIDValue: 1}, + ) + + sel := roastSigningParticipantSelector{} + members := []chain.Address{ + chain.Address("op-1"), + chain.Address("op-2"), + chain.Address("op-3"), + chain.Address("op-4"), + chain.Address("op-5"), + } + got, err := sel.Select(members, 42, 0, 3, "session-no-handle") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(got) < 3 { + t.Fatalf("expected legacy fallback; got %d members", len(got)) + } +} + +func TestMembersResolver_MapsIndexToAddress(t *testing.T) { + members := []chain.Address{ + chain.Address("op-1"), + chain.Address("op-2"), + chain.Address("op-3"), + } + r := membersResolver(members) + for i := 1; i <= 3; i++ { + got, err := r.For(group.MemberIndex(i)) + if err != nil { + t.Fatalf("For(%d): %v", i, err) + } + want := members[i-1] + if got != want { + t.Fatalf("For(%d) = %q, want %q", i, got, want) + } + } +} + +func TestMembersResolver_RejectsZeroIndex(t *testing.T) { + r := membersResolver([]chain.Address{chain.Address("op-1")}) + _, err := r.For(0) + if err == nil { + t.Fatal("expected error for zero member index") + } +} + +func TestMembersResolver_RejectsOutOfRangeIndex(t *testing.T) { + r := membersResolver([]chain.Address{chain.Address("op-1")}) + _, err := r.For(99) + if err == nil { + t.Fatal("expected error for out-of-range index") + } +} + +func TestROASTSelector_UsesBundleWhenAllConditionsMet(t *testing.T) { + signing.ResetTransitionBundleRegistryForTest() + signing.ResetRoastRetryRegistrationForTest() + signing.ResetSessionHandleRegistryForTest() + t.Cleanup(signing.ResetTransitionBundleRegistryForTest) + t.Cleanup(signing.ResetRoastRetryRegistrationForTest) + t.Cleanup(signing.ResetSessionHandleRegistryForTest) + + // Build a real coordinator and run through the bundle-production + // flow end-to-end, then verify the selector consumes the bundle + // and returns the IncludedSet mapped to addresses. + ctx, _ := attempt.NewAttemptContext( + "session-with-bundle", + "key-group", + []byte{0x01, 0x02, 0x03}, + [attempt.MessageDigestLength]byte{0xab}, + 0, + []group.MemberIndex{1, 2, 3, 4, 5}, + nil, + ) + + scratch := roast.NewInMemoryCoordinator() + hScratch, _ := scratch.BeginAttempt(ctx) + elected, _ := scratch.SelectedCoordinator(hScratch) + + coord := roast.NewInMemoryCoordinatorWithSigning( + elected, roast.NoOpSigner(), roast.NoOpSignatureVerifier(), + ) + signing.RegisterRoastRetryCoordinator(signing.RoastRetryDeps{ + Coordinator: coord, + Signer: roast.NoOpSigner(), + Verifier: roast.NoOpSignatureVerifier(), + SelfMember: uint32(elected), + }) + + handle, _ := coord.BeginAttempt(ctx) + signing.SetCurrentAttemptHandleForSession("session-with-bundle", handle, ctx) + + // Seed every member's snapshot so AggregateBundle has content. + for _, m := range ctx.IncludedSet { + snap := roast.NewLocalEvidenceSnapshot(m, ctx.Hash(), attempt.Evidence{}) + snap.OperatorSignature = []byte{0x01} + if err := coord.RecordEvidence(handle, snap); err != nil { + t.Fatalf("record %d: %v", m, err) + } + } + bundle, err := coord.AggregateBundle(handle) + if err != nil { + t.Fatalf("aggregate: %v", err) + } + signing.RecordTransitionBundleForSession("session-with-bundle", bundle) + + sel := roastSigningParticipantSelector{} + members := []chain.Address{ + chain.Address("op-1"), + chain.Address("op-2"), + chain.Address("op-3"), + chain.Address("op-4"), + chain.Address("op-5"), + } + got, err := sel.Select(members, 0, 0, 3, "session-with-bundle") + if err != nil { + t.Fatalf("select: %v", err) + } + if len(got) == 0 { + t.Fatal("selector must return at least one address") + } +} 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_native_backend_frost_native_test.go b/pkg/tbtc/signing_native_backend_frost_native_test.go new file mode 100644 index 0000000000..df6afaadd9 --- /dev/null +++ b/pkg/tbtc/signing_native_backend_frost_native_test.go @@ -0,0 +1,899 @@ +//go:build frost_native + +package tbtc + +import ( + "bytes" + "context" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "strconv" + "strings" + "sync" + "sync/atomic" + "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" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +type countingNativeExecutionFFISigningPrimitive struct { + signCalls atomic.Int64 +} + +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 +} + +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, + 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( + ctx context.Context, + logger log.StandardLogger, + request *frostsigning.NativeExecutionFFISigningRequest, +) (*frost.Signature, error) { + cnefsp.signCalls.Add(1) + return &frost.Signature{}, nil +} + +func (cnefsp *countingNativeExecutionFFISigningPrimitive) RegisterUnmarshallers( + channel net.BroadcastChannel, +) { +} + +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.NativeSignerMaterialFormatFrostTBTCSignerV1 { + 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 (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 (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, + taprootMerkleRoot *[32]byte, +) (*frostsigning.NativeTBTCSignerRoundState, error) { + _ = taprootMerkleRoot + + 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, + taprootMerkleRoot *[32]byte, +) ([]byte, error) { + _ = taprootMerkleRoot + + 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) 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() + + 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() + 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 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.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) + t.Cleanup(frostsigning.UnregisterNativeExecutionAdapter) + t.Cleanup(frostsigning.UnregisterNativeExecutionBridge) + t.Cleanup(frostsigning.UnregisterNativeExecutionFFIExecutor) + + 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) + + 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 { + 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) + } + + // 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, + 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_FFIStrictBackend_WithNativeSignerMaterial( + t *testing.T, +) { + executor := setupSigningExecutor(t) + configureSignersWithTBTCSignerMaterial(t, executor, 0) + + 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"}) + 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) + } + + 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 { + t.Fatal("wrong end block") + } +} + +func TestSigningExecutor_Sign_NativeBackend_FallsBackWhenOnlyLegacySignerMaterial( + t *testing.T, +) { + 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() + 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.Load() != 0 { + t.Fatalf( + "unexpected native primitive sign calls count\nexpected: [%d]\nactual: [%d]", + 0, + primitive.signCalls.Load(), + ) + } + + 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_FFIStrictBackend_AttemptVariationChangesCohortSelection( + t *testing.T, +) { + executor := setupSigningExecutor(t) + configureSignersWithTBTCSignerMaterial(t, executor, 0) + + 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 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) + + 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() + 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) + 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 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 +} 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") + } +} diff --git a/pkg/tbtc/signing_schnorr_default.go b/pkg/tbtc/signing_schnorr_default.go new file mode 100644 index 0000000000..3074107327 --- /dev/null +++ b/pkg/tbtc/signing_schnorr_default.go @@ -0,0 +1,46 @@ +//go:build !frost_native + +package tbtc + +import ( + "encoding/json" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func signingMaterialUsesSchnorrSignatures(signingMaterial any) bool { + switch material := signingMaterial.(type) { + case *tecdsa.PrivateKeyShare: + return false + case *frostsigning.NativeSignerMaterial: + return nativeSignerMaterialUsesSchnorrSignaturesDefault(material) + case frostsigning.NativeSignerMaterial: + return nativeSignerMaterialUsesSchnorrSignaturesDefault(&material) + default: + return true + } +} + +func nativeSignerMaterialUsesSchnorrSignaturesDefault( + material *frostsigning.NativeSignerMaterial, +) bool { + if material == nil { + return true + } + + switch material.Format { + case frostsigning.NativeSignerMaterialFormatFrostUniFFIV1: + return false + case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: + var payload frostsigning.NativeTBTCSignerMaterialPayload + if err := json.Unmarshal(material.Payload, &payload); err != nil { + return true + } + + return payload.KeyGroupSource != + frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey + default: + return true + } +} diff --git a/pkg/tbtc/signing_schnorr_default_test.go b/pkg/tbtc/signing_schnorr_default_test.go new file mode 100644 index 0000000000..0903830547 --- /dev/null +++ b/pkg/tbtc/signing_schnorr_default_test.go @@ -0,0 +1,92 @@ +//go:build !frost_native + +package tbtc + +import ( + "encoding/json" + "testing" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestSigningMaterialUsesSchnorrSignatures_Default(t *testing.T) { + tbtcSignerPayload := func(t *testing.T, keyGroupSource string) []byte { + t.Helper() + + payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ + KeyGroup: "key-group", + KeyGroupSource: keyGroupSource, + }) + if err != nil { + t.Fatalf("cannot marshal tbtc-signer payload: [%v]", err) + } + + return payload + } + + tests := map[string]struct { + material any + expectSchnorr bool + }{ + "legacy tecdsa private key share": { + material: &tecdsa.PrivateKeyShare{}, + expectSchnorr: false, + }, + "legacy frost uniffi v1 material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte{0x01}, + }, + expectSchnorr: false, + }, + "legacy tbtc-signer scaffold material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: tbtcSignerPayload( + t, + frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + ), + }, + expectSchnorr: false, + }, + "native tbtc-signer material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: tbtcSignerPayload(t, "dkg-persisted"), + }, + expectSchnorr: true, + }, + "malformed tbtc-signer material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte("not-json"), + }, + expectSchnorr: true, + }, + "unknown native material": { + material: &frostsigning.NativeSignerMaterial{ + Format: "unknown", + Payload: []byte{0x01}, + }, + expectSchnorr: true, + }, + "unknown material": { + material: struct{}{}, + expectSchnorr: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := signingMaterialUsesSchnorrSignatures(test.material) + if actual != test.expectSchnorr { + t.Fatalf( + "unexpected Schnorr classification\nexpected: [%v]\nactual: [%v]", + test.expectSchnorr, + actual, + ) + } + }) + } +} diff --git a/pkg/tbtc/signing_schnorr_frost_native.go b/pkg/tbtc/signing_schnorr_frost_native.go new file mode 100644 index 0000000000..62860478c1 --- /dev/null +++ b/pkg/tbtc/signing_schnorr_frost_native.go @@ -0,0 +1,48 @@ +//go:build frost_native + +package tbtc + +import ( + "encoding/json" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func signingMaterialUsesSchnorrSignatures(signingMaterial any) bool { + switch material := signingMaterial.(type) { + case *tecdsa.PrivateKeyShare: + return false + case *frostsigning.NativeSignerMaterial: + return nativeSignerMaterialUsesSchnorrSignatures(material) + case frostsigning.NativeSignerMaterial: + return nativeSignerMaterialUsesSchnorrSignatures(&material) + default: + return true + } +} + +func nativeSignerMaterialUsesSchnorrSignatures( + material *frostsigning.NativeSignerMaterial, +) bool { + if material == nil { + return true + } + + switch material.Format { + case frostsigning.NativeSignerMaterialFormatFrostUniFFIV1: + return false + case frostsigning.NativeSignerMaterialFormatFrostUniFFIV2: + return true + case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: + var payload frostsigning.NativeTBTCSignerMaterialPayload + if err := json.Unmarshal(material.Payload, &payload); err != nil { + return true + } + + return payload.KeyGroupSource != + frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey + default: + return true + } +} diff --git a/pkg/tbtc/signing_schnorr_frost_native_test.go b/pkg/tbtc/signing_schnorr_frost_native_test.go new file mode 100644 index 0000000000..e411242963 --- /dev/null +++ b/pkg/tbtc/signing_schnorr_frost_native_test.go @@ -0,0 +1,92 @@ +//go:build frost_native + +package tbtc + +import ( + "encoding/json" + "testing" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" + "github.com/keep-network/keep-core/pkg/tecdsa" +) + +func TestSigningMaterialUsesSchnorrSignatures_FrostNative(t *testing.T) { + tbtcSignerPayload := func(t *testing.T, keyGroupSource string) []byte { + t.Helper() + + payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ + KeyGroup: "key-group", + KeyGroupSource: keyGroupSource, + }) + if err != nil { + t.Fatalf("cannot marshal tbtc-signer payload: [%v]", err) + } + + return payload + } + + tests := map[string]struct { + material any + expectSchnorr bool + }{ + "legacy tecdsa private key share": { + material: &tecdsa.PrivateKeyShare{}, + expectSchnorr: false, + }, + "legacy frost uniffi v1 material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV1, + Payload: []byte{0x01}, + }, + expectSchnorr: false, + }, + "legacy tbtc-signer scaffold material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: tbtcSignerPayload( + t, + frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + ), + }, + expectSchnorr: false, + }, + "unsupported uniffi frost material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV2, + Payload: []byte{0x01}, + }, + expectSchnorr: true, + }, + "native tbtc-signer material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: tbtcSignerPayload(t, "dkg-persisted"), + }, + expectSchnorr: true, + }, + "malformed tbtc-signer material": { + material: &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: []byte("not-json"), + }, + expectSchnorr: true, + }, + "unknown material": { + material: struct{}{}, + expectSchnorr: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := signingMaterialUsesSchnorrSignatures(test.material) + if actual != test.expectSchnorr { + t.Fatalf( + "unexpected Schnorr classification\nexpected: [%v]\nactual: [%v]", + test.expectSchnorr, + actual, + ) + } + }) + } +} diff --git a/pkg/tbtc/signing_test.go b/pkg/tbtc/signing_test.go index 9298ad7d7f..de12bc9ddd 100644 --- a/pkg/tbtc/signing_test.go +++ b/pkg/tbtc/signing_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ecdsa" "math/big" + "strings" "testing" "time" @@ -19,6 +20,68 @@ import ( "github.com/keep-network/keep-core/pkg/tecdsa" ) +func TestSigningSessionID_LegacyFormat(t *testing.T) { + message, ok := new(big.Int).SetString( + "ac692bb7fddf3f7e1e050a83cf3ffb6e8e69888ce980281aa39da169525750ef", + 16, + ) + if !ok { + t.Fatal("failed to build test message") + } + + sessionID := signingSessionID(message, nil, 25300, 12) + + expected := "ac692bb7fddf3f7e1e050a83cf3ffb6e8e69888ce980281aa39da169525750ef-12" + if sessionID != expected { + t.Fatalf( + "unexpected signing session ID\nexpected: [%s]\nactual: [%s]", + expected, + sessionID, + ) + } +} + +func TestSigningSessionID_TaprootFormatStaysWithinSignerLimit(t *testing.T) { + message, ok := new(big.Int).SetString( + "ac692bb7fddf3f7e1e050a83cf3ffb6e8e69888ce980281aa39da169525750ef", + 16, + ) + if !ok { + t.Fatal("failed to build test message") + } + + var merkleRoot [32]byte + for i := range merkleRoot { + merkleRoot[i] = byte(i + 1) + } + + sessionID := signingSessionID(message, &merkleRoot, 25300, 12) + + if len(sessionID) > 128 { + t.Fatalf("Taproot signing session ID exceeds signer limit: [%d]", len(sessionID)) + } + if !strings.HasPrefix(sessionID, "tr-") { + t.Fatalf("unexpected Taproot signing session ID prefix: [%s]", sessionID) + } + if !strings.HasSuffix(sessionID, "-12") { + t.Fatalf("unexpected Taproot signing session ID attempt suffix: [%s]", sessionID) + } + + changedMerkleRoot := merkleRoot + changedMerkleRoot[0] ^= 0xff + if signingSessionID(message, &changedMerkleRoot, 25300, 12) == sessionID { + t.Fatal("expected Taproot signing session ID to bind the merkle root") + } + + if signingSessionID(message, &merkleRoot, 25300, 13) == sessionID { + t.Fatal("expected Taproot signing session ID to bind the attempt number") + } + + if signingSessionID(message, &merkleRoot, 28900, 12) == sessionID { + t.Fatal("expected Taproot signing session ID to bind the signing start block") + } +} + func TestSigningExecutor_Sign(t *testing.T) { executor := setupSigningExecutor(t) @@ -38,8 +101,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 +162,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) } @@ -110,6 +173,14 @@ func TestSigningExecutor_SignBatch(t *testing.T) { // setupSigningExecutor sets up an instance of the signing executor ready // to perform test signing. func setupSigningExecutor(t *testing.T) *signingExecutor { + // Tests in this suite exercise the keep-tbtc signing executor against + // in-process tECDSA fixtures. Under the `frost_native frost_tbtc_signer` + // build tags, the signer-material resolver refuses scaffold-era + // (legacy-wallet-pubkey) material by default; the fixtures here are + // inherently scaffold-era so the executor needs the operator opt-in to + // continue running. Production deployments must never set this env var. + t.Setenv("KEEP_CORE_FROST_TBTC_SIGNER_ACCEPT_SCAFFOLD_KEY_GROUP", "true") + groupParameters := &GroupParameters{ GroupSize: 5, GroupQuorum: 4, diff --git a/pkg/tbtc/taproot_test_helpers_test.go b/pkg/tbtc/taproot_test_helpers_test.go new file mode 100644 index 0000000000..7c6d302702 --- /dev/null +++ b/pkg/tbtc/taproot_test_helpers_test.go @@ -0,0 +1,83 @@ +package tbtc + +import ( + "crypto/ecdsa" + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/keep-network/keep-core/pkg/bitcoin" +) + +func testWalletPublicKeyFromXOnly(t *testing.T, xOnlyHex string) *ecdsa.PublicKey { + t.Helper() + + xOnlyBytes, err := hex.DecodeString(xOnlyHex) + if err != nil { + t.Fatalf("cannot decode x-only key: [%v]", err) + } + + compressedPublicKey := append([]byte{0x02}, xOnlyBytes...) + parsedPublicKey, err := btcec.ParsePubKey(compressedPublicKey, btcec.S256()) + if err != nil { + t.Fatalf("cannot parse compressed public key: [%v]", err) + } + + return &ecdsa.PublicKey{ + Curve: btcec.S256(), + X: parsedPublicKey.X, + Y: parsedPublicKey.Y, + } +} + +func testTaprootWalletMainUtxo( + t *testing.T, + bitcoinChain bitcoin.Chain, + walletPublicKey *ecdsa.PublicKey, +) *bitcoin.UnspentTransactionOutput { + t.Helper() + + walletXOnlyPublicKey, err := walletXOnlyPublicKey(walletPublicKey) + if err != nil { + t.Fatalf("cannot extract wallet x-only public key: [%v]", err) + } + + taprootScript, err := bitcoin.PayToTaproot(walletXOnlyPublicKey) + if err != nil { + t.Fatalf("cannot compute Taproot wallet script: [%v]", err) + } + + var previousTxHash bitcoin.Hash + previousTxHash[0] = 0x01 + + fundingTx := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: previousTxHash, + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 100000, + PublicKeyScript: taprootScript, + }, + }, + } + + if err := bitcoinChain.BroadcastTransaction(fundingTx); err != nil { + t.Fatalf("cannot broadcast Taproot wallet main UTXO transaction: [%v]", err) + } + + return &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTx.Hash(), + OutputIndex: 0, + }, + Value: 100000, + } +} diff --git a/pkg/tbtc/taproot_wallet.go b/pkg/tbtc/taproot_wallet.go new file mode 100644 index 0000000000..51c4653cd3 --- /dev/null +++ b/pkg/tbtc/taproot_wallet.go @@ -0,0 +1,20 @@ +package tbtc + +import ( + "crypto/ecdsa" + "fmt" + + "github.com/keep-network/keep-core/pkg/internal/byteutils" +) + +func walletXOnlyPublicKey(walletPublicKey *ecdsa.PublicKey) ([32]byte, error) { + x, err := byteutils.LeftPadTo32Bytes(walletPublicKey.X.Bytes()) + if err != nil { + return [32]byte{}, fmt.Errorf("cannot encode wallet x-only key: [%w]", err) + } + + var result [32]byte + copy(result[:], x) + + return result, nil +} diff --git a/pkg/tbtc/tbtc.go b/pkg/tbtc/tbtc.go index 62b226aed6..3e48edd808 100644 --- a/pkg/tbtc/tbtc.go +++ b/pkg/tbtc/tbtc.go @@ -65,6 +65,22 @@ 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. + // `native` allows transitional legacy fallback when native cryptographic + // execution is unavailable. `ffi` requires native execution and does not + // allow fallback. + FrostSigningBackend string + // DisableLegacyECDSA skips legacy ECDSA wallet DKG handling and pre-params + // generation. This is intended for FROST-only deployments where wallet + // creation and signing are handled by the FROST registry and signer. + DisableLegacyECDSA bool + // DisableLegacySortitionPoolMonitoring skips monitoring and auto-joining + // the legacy ECDSA sortition pool. This is intended for FROST-only + // deployments where operators are authorized through FrostAllowlist and no + // longer have TokenStaking-backed ECDSA operator state. + DisableLegacySortitionPoolMonitoring bool } // Initialize kicks off the TBTC by initializing internal state, ensuring @@ -111,12 +127,20 @@ func Initialize( deduplicator := newDeduplicator() + if frostChain, ok := chain.(FrostDKGChain); ok { + initializeFrostDKGCoordinator(ctx, node, frostChain) + } + if clientInfo != nil { // only if client info endpoint is configured clientInfo.ObserveApplicationSource( "tbtc", map[string]clientinfo.Source{ "pre_params_count": func() float64 { + if node.dkgExecutor == nil { + return 0 + } + return float64(node.dkgExecutor.preParamsCount()) }, }, @@ -147,147 +171,155 @@ func Initialize( ) } - err = sortition.MonitorPool( - ctx, - logger, - chain, - sortition.DefaultStatusCheckTick, - sortition.NewConjunctionPolicy( - sortition.NewBetaOperatorPolicy(chain, logger), - &enoughPreParamsInPoolPolicy{ - node: node, - config: config, - }, - ), - ) - if err != nil { - return fmt.Errorf( - "could not set up sortition pool monitoring: [%v]", - err, + if shouldMonitorLegacySortitionPool(config) { + err = sortition.MonitorPool( + ctx, + logger, + chain, + sortition.DefaultStatusCheckTick, + sortition.NewConjunctionPolicy( + sortition.NewBetaOperatorPolicy(chain, logger), + &enoughPreParamsInPoolPolicy{ + node: node, + config: config, + }, + ), ) + if err != nil { + return fmt.Errorf( + "could not set up sortition pool monitoring: [%v]", + err, + ) + } + } else { + logger.Infof("legacy ECDSA sortition pool monitoring disabled") } - _ = chain.OnDKGStarted(func(event *DKGStartedEvent) { - go func() { - if ok := deduplicator.notifyDKGStarted( - event.Seed, - ); !ok { - logger.Infof( - "DKG started event with seed [0x%x] has been "+ - "already processed", + if shouldRunLegacyECDSA(config) { + _ = chain.OnDKGStarted(func(event *DKGStartedEvent) { + go func() { + if ok := deduplicator.notifyDKGStarted( event.Seed, - ) - return - } - - confirmationBlock := event.BlockNumber + dkgStartedConfirmationBlocks - - logger.Infof( - "observed DKG started event with seed [0x%x] and "+ - "starting block [%v]; waiting for block [%v] to confirm", - event.Seed, - event.BlockNumber, - confirmationBlock, - ) - - err := node.waitForBlockHeight(ctx, confirmationBlock) - if err != nil { - logger.Errorf("failed to confirm DKG started event: [%v]", err) - return - } + ); !ok { + logger.Infof( + "DKG started event with seed [0x%x] has been "+ + "already processed", + event.Seed, + ) + return + } - dkgState, err := chain.GetDKGState() - if err != nil { - logger.Errorf("failed to check DKG state: [%v]", err) - return - } + confirmationBlock := event.BlockNumber + dkgStartedConfirmationBlocks - if dkgState == AwaitingResult { - // Fetch all past DKG started events starting from one - // confirmation period before the original event's block. - // If there was a chain reorg, the event we received could be - // moved to a block with a lower number than the one - // we received. - pastEvents, err := chain.PastDKGStartedEvents( - &DKGStartedEventFilter{ - StartBlock: event.BlockNumber - dkgStartedConfirmationBlocks, - }, + logger.Infof( + "observed DKG started event with seed [0x%x] and "+ + "starting block [%v]; waiting for block [%v] to confirm", + event.Seed, + event.BlockNumber, + confirmationBlock, ) + + err := node.waitForBlockHeight(ctx, confirmationBlock) if err != nil { - logger.Errorf("failed to get past DKG started events: [%v]", err) + logger.Errorf("failed to confirm DKG started event: [%v]", err) return } - // Should not happen but just in case. - if len(pastEvents) == 0 { - logger.Errorf("no past DKG started events") + dkgState, err := chain.GetDKGState() + if err != nil { + logger.Errorf("failed to check DKG state: [%v]", err) return } - lastEvent := pastEvents[len(pastEvents)-1] - - logger.Infof( - "DKG started with seed [0x%x] at block [%v]", - lastEvent.Seed, - lastEvent.BlockNumber, - ) + if dkgState == AwaitingResult { + // Fetch all past DKG started events starting from one + // confirmation period before the original event's block. + // If there was a chain reorg, the event we received could be + // moved to a block with a lower number than the one + // we received. + pastEvents, err := chain.PastDKGStartedEvents( + &DKGStartedEventFilter{ + StartBlock: event.BlockNumber - dkgStartedConfirmationBlocks, + }, + ) + if err != nil { + logger.Errorf("failed to get past DKG started events: [%v]", err) + return + } + + // Should not happen but just in case. + if len(pastEvents) == 0 { + logger.Errorf("no past DKG started events") + return + } + + lastEvent := pastEvents[len(pastEvents)-1] + + logger.Infof( + "DKG started with seed [0x%x] at block [%v]", + lastEvent.Seed, + lastEvent.BlockNumber, + ) + + // The off-chain protocol should be started as close as possible + // to the current block or even further. Starting the off-chain + // protocol with a past block will likely cause a failure of the + // first attempt as the start block is used to synchronize + // the announcements and the state machine. Here we ensure + // a proper start point by delaying the execution by the + // confirmation period length. + node.joinDKGIfEligible( + lastEvent.Seed, + lastEvent.BlockNumber, + dkgStartedConfirmationBlocks, + ) + } else { + logger.Infof( + "DKG started event with seed [0x%x] and starting "+ + "block [%v] was not confirmed", + event.Seed, + event.BlockNumber, + ) + } + }() + }) - // The off-chain protocol should be started as close as possible - // to the current block or even further. Starting the off-chain - // protocol with a past block will likely cause a failure of the - // first attempt as the start block is used to synchronize - // the announcements and the state machine. Here we ensure - // a proper start point by delaying the execution by the - // confirmation period length. - node.joinDKGIfEligible( - lastEvent.Seed, - lastEvent.BlockNumber, - dkgStartedConfirmationBlocks, - ) - } else { - logger.Infof( - "DKG started event with seed [0x%x] and starting "+ - "block [%v] was not confirmed", + _ = chain.OnDKGResultSubmitted(func(event *DKGResultSubmittedEvent) { + go func() { + if ok := deduplicator.notifyDKGResultSubmitted( event.Seed, + event.ResultHash, event.BlockNumber, - ) - } - }() - }) + ); !ok { + logger.Warnf( + "Result with hash [0x%x] for DKG with seed [0x%x] "+ + "and starting block [%v] has been already processed", + event.ResultHash, + event.Seed, + event.BlockNumber, + ) + return + } - _ = chain.OnDKGResultSubmitted(func(event *DKGResultSubmittedEvent) { - go func() { - if ok := deduplicator.notifyDKGResultSubmitted( - event.Seed, - event.ResultHash, - event.BlockNumber, - ); !ok { - logger.Warnf( + logger.Infof( "Result with hash [0x%x] for DKG with seed [0x%x] "+ - "and starting block [%v] has been already processed", + "submitted at block [%v]", event.ResultHash, event.Seed, event.BlockNumber, ) - return - } - - logger.Infof( - "Result with hash [0x%x] for DKG with seed [0x%x] "+ - "submitted at block [%v]", - event.ResultHash, - event.Seed, - event.BlockNumber, - ) - node.validateDKG( - event.Seed, - event.BlockNumber, - event.Result, - event.ResultHash, - ) - }() - }) + node.validateDKG( + event.Seed, + event.BlockNumber, + event.Result, + event.ResultHash, + ) + }() + }) + } else { + logger.Infof("legacy ECDSA wallet DKG disabled") + } _ = chain.OnWalletClosed(func(event *WalletClosedEvent) { go func() { @@ -326,6 +358,15 @@ func Initialize( return nil } +func shouldMonitorLegacySortitionPool(config Config) bool { + return shouldRunLegacyECDSA(config) && + !config.DisableLegacySortitionPoolMonitoring +} + +func shouldRunLegacyECDSA(config Config) bool { + return !config.DisableLegacyECDSA +} + // enoughPreParamsInPoolPolicy is a policy that enforces the sufficient size // of the DKG pre-parameters pool before joining the sortition pool. type enoughPreParamsInPoolPolicy struct { diff --git a/pkg/tbtc/tbtc_test.go b/pkg/tbtc/tbtc_test.go new file mode 100644 index 0000000000..f5f6878896 --- /dev/null +++ b/pkg/tbtc/tbtc_test.go @@ -0,0 +1,31 @@ +package tbtc + +import "testing" + +func TestShouldMonitorLegacySortitionPool(t *testing.T) { + if !shouldMonitorLegacySortitionPool(Config{}) { + t.Fatal("expected legacy sortition pool monitoring to be enabled by default") + } + + if shouldMonitorLegacySortitionPool(Config{ + DisableLegacySortitionPoolMonitoring: true, + }) { + t.Fatal("expected legacy sortition pool monitoring to be disabled") + } + + if shouldMonitorLegacySortitionPool(Config{ + DisableLegacyECDSA: true, + }) { + t.Fatal("expected FROST-only mode to disable legacy sortition pool monitoring") + } +} + +func TestShouldRunLegacyECDSA(t *testing.T) { + if !shouldRunLegacyECDSA(Config{}) { + t.Fatal("expected legacy ECDSA to run by default") + } + + if shouldRunLegacyECDSA(Config{DisableLegacyECDSA: true}) { + t.Fatal("expected legacy ECDSA to be disabled") + } +} diff --git a/pkg/tbtc/wallet.go b/pkg/tbtc/wallet.go index 9ad8b7e06f..b65e373fd1 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" @@ -17,11 +19,17 @@ 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" ) +type unsignedTransactionInputReference struct { + TxIDHex string + Vout uint32 +} + // WalletActionType represents actions types that can be performed by a wallet. type WalletActionType uint8 @@ -281,7 +289,20 @@ type walletSigningExecutor interface { ctx context.Context, messages []*big.Int, startBlock uint64, - ) ([]*tecdsa.Signature, error) + ) ([]*frost.Signature, error) +} + +type schnorrWalletSigningExecutor interface { + usesSchnorrSignatures() bool +} + +type taprootTweakedWalletSigningExecutor interface { + signBatchWithTaprootMerkleRoots( + ctx context.Context, + messages []*big.Int, + taprootMerkleRoots []*[32]byte, + startBlock uint64, + ) ([]*frost.Signature, error) } // walletTransactionExecutor is a component allowing to sign and broadcast @@ -295,6 +316,11 @@ type walletTransactionExecutor struct { waitForBlockFn waitForBlockFn } +var buildTaprootTxViaNativeSignerFn = buildTaprootTxViaNativeSigner +var nativeBuildTaprootTxSigningSubstitutionEnabledFn = nativeBuildTaprootTxSigningSubstitutionEnabled + +const nativeBuildTaprootTxSigningSubstitutionEnvVar = "KEEP_CORE_NATIVE_BUILDTX_SIGNING_SUBSTITUTION" + func newWalletTransactionExecutor( btcChain bitcoin.Chain, executingWallet wallet, @@ -318,6 +344,49 @@ func (wte *walletTransactionExecutor) signTransaction( signingStartBlock uint64, signingTimeoutBlock uint64, ) (*bitcoin.Transaction, error) { + substitutionEnabled := nativeBuildTaprootTxSigningSubstitutionEnabledFn() + + nativeUnsignedTxHex, err := buildTaprootTxViaNativeSignerFn(unsignedTx) + if err != nil { + return nil, fmt.Errorf( + "error while building unsigned transaction with native tbtc-signer: [%w]", + err, + ) + } + + if nativeUnsignedTxHex != "" { + signTxLogger.Debugf( + "received unsigned transaction from native tbtc-signer BuildTaprootTx [txHexLen:%d]", + len(nativeUnsignedTxHex), + ) + + nativeUnsignedTx, err := evaluateNativeUnsignedTransactionForSigning( + signTxLogger, + nativeUnsignedTxHex, + unsignedTx.UnsignedTransaction(), + 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", + ) + } + } + signTxLogger.Infof("computing transaction's sig hashes") sigHashes, err := unsignedTx.ComputeSignatureHashes() @@ -328,6 +397,20 @@ func (wte *walletTransactionExecutor) signTransaction( ) } + if unsignedTx.HasTaprootKeyPathInputs() && + !unsignedTx.HasOnlyTaprootKeyPathInputs() { + return nil, fmt.Errorf( + "cannot apply FROST signatures to mixed taproot and legacy inputs", + ) + } + + if wte.usesSchnorrSignatures() && + !unsignedTx.HasOnlyTaprootKeyPathInputs() { + return nil, fmt.Errorf( + "cannot apply FROST signatures to non-taproot transaction inputs", + ) + } + signTxLogger.Infof("signing transaction's sig hashes") signingCtx, cancelSigningCtx := withCancelOnBlock( @@ -337,11 +420,29 @@ func (wte *walletTransactionExecutor) signTransaction( ) defer cancelSigningCtx() - signatures, err := wte.signingExecutor.signBatch( - signingCtx, - sigHashes, - signingStartBlock, - ) + var signatures []*frost.Signature + taprootMerkleRoots := unsignedTx.TaprootKeyPathInputMerkleRoots() + if hasTaprootMerkleRoots(taprootMerkleRoots) { + tweakedSigningExecutor, ok := wte.signingExecutor.(taprootTweakedWalletSigningExecutor) + if !ok { + return nil, fmt.Errorf( + "taproot tweaked signing requires signer support", + ) + } + + signatures, err = tweakedSigningExecutor.signBatchWithTaprootMerkleRoots( + signingCtx, + sigHashes, + taprootMerkleRoots, + signingStartBlock, + ) + } else { + signatures, err = wte.signingExecutor.signBatch( + signingCtx, + sigHashes, + signingStartBlock, + ) + } if err != nil { return nil, fmt.Errorf( "error while signing transaction's sig hashes: [%v]", @@ -351,11 +452,36 @@ func (wte *walletTransactionExecutor) signTransaction( signTxLogger.Infof("applying transaction's signatures") + if unsignedTx.HasTaprootKeyPathInputs() { + containers := make( + []*bitcoin.SchnorrSignatureContainer, + len(signatures), + ) + for i, signature := range signatures { + containers[i] = &bitcoin.SchnorrSignatureContainer{ + Signature: signature.Serialize(), + } + } + + tx, err := unsignedTx.AddTaprootKeyPathSignatures(containers) + if err != nil { + return nil, fmt.Errorf( + "error while applying transaction's taproot key-path "+ + "signatures: [%v]", + err, + ) + } + + signTxLogger.Infof("transaction created successfully") + + return tx, nil + } + 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, } } @@ -373,6 +499,324 @@ func (wte *walletTransactionExecutor) signTransaction( return tx, nil } +func (wte *walletTransactionExecutor) usesSchnorrSignatures() bool { + executor, ok := wte.signingExecutor.(schnorrWalletSigningExecutor) + if !ok { + return false + } + + return executor.usesSchnorrSignatures() +} + +func hasTaprootMerkleRoots(taprootMerkleRoots []*[32]byte) bool { + for _, merkleRoot := range taprootMerkleRoots { + if merkleRoot != nil { + return true + } + } + + return false +} + +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, + expectedTransaction *bitcoin.Transaction, + 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 with Go builder state: [%v]", + err, + ) + return nil, nil + } + + diverges, divergenceReason, err := nativeUnsignedTransactionDivergesFromTransaction( + nativeUnsignedTx, + expectedTransaction, + ) + if err != nil { + if substitutionEnabled { + return nil, err + } + + signTxLogger.Warnf( + "cannot compare native BuildTaprootTx unsigned transaction with Go builder state: [%v]", + err, + ) + return nil, nil + } + + if diverges { + divergenceMessage := "native BuildTaprootTx unsigned transaction diverges from Go builder state" + if divergenceReason != "" { + divergenceMessage = fmt.Sprintf( + "%s: %s", + divergenceMessage, + divergenceReason, + ) + } + + if substitutionEnabled { + return nil, fmt.Errorf("%s", divergenceMessage) + } + + signTxLogger.Warnf(divergenceMessage) + } + + if substitutionEnabled { + return nativeUnsignedTx, nil + } + + return nil, nil +} + +func decodeNativeUnsignedTransactionHex( + nativeUnsignedTxHex string, +) (*bitcoin.Transaction, error) { + nativeUnsignedTxBytes, err := hex.DecodeString(nativeUnsignedTxHex) + if err != nil { + return nil, fmt.Errorf("cannot decode native tx hex: [%w]", err) + } + + nativeUnsignedTx := &bitcoin.Transaction{} + if err := nativeUnsignedTx.Deserialize(nativeUnsignedTxBytes); err != nil { + return nil, fmt.Errorf("cannot deserialize native tx bytes: [%w]", err) + } + + return nativeUnsignedTx, nil +} + +func nativeUnsignedTransactionDivergesFromTransaction( + nativeUnsignedTx *bitcoin.Transaction, + expectedTransaction *bitcoin.Transaction, +) (bool, string, error) { + actualShape, err := extractUnsignedTransactionShapeFromTransaction(nativeUnsignedTx) + if err != nil { + return false, "", err + } + + expectedShape, err := extractUnsignedTransactionShapeFromTransaction(expectedTransaction) + if err != nil { + 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 "", false +} + +type unsignedTransactionShape struct { + Version int32 + Locktime uint32 + InputReferences []unsignedTransactionInputReference + InputSequences []uint32 + Outputs []bitcoin.UnsignedTransactionOutput +} + +func extractUnsignedTransactionShapeFromTransaction( + transaction *bitcoin.Transaction, +) (*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, fmt.Errorf("transaction input [%d] is nil", i) + } + + if input.Outpoint == nil { + return 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, + }, + ) + inputSequences = append(inputSequences, input.Sequence) + } + + outputs := make([]bitcoin.UnsignedTransactionOutput, 0, len(transaction.Outputs)) + for i, output := range transaction.Outputs { + if output == nil { + return nil, fmt.Errorf("transaction output [%d] is nil", i) + } + + if output.Value < 0 { + return 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 &unsignedTransactionShape{ + Version: transaction.Version, + Locktime: transaction.Locktime, + InputReferences: inputReferences, + InputSequences: inputSequences, + Outputs: outputs, + }, nil +} + +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 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 + } + + 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 "", 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 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 "", false +} + // broadcastTransaction broadcasts a signed Bitcoin transaction until // the transaction lands in the Bitcoin mempool or the provided timeout // is hit, whichever comes first. @@ -519,6 +963,48 @@ func DetermineWalletMainUtxo( walletPublicKeyHash [20]byte, bridgeChain BridgeChain, btcChain bitcoin.Chain, +) (*bitcoin.UnspentTransactionOutput, error) { + walletScripts, err := legacyWalletPublicKeyScripts(walletPublicKeyHash) + if err != nil { + return nil, err + } + + return determineWalletMainUtxo( + walletPublicKeyHash, + walletScripts, + bridgeChain, + btcChain, + ) +} + +// DetermineWalletMainUtxoForPublicKey determines the plain-text wallet main +// UTXO currently registered in the Bridge on-chain contract. Unlike +// DetermineWalletMainUtxo, this variant can discover Taproot wallet outputs. +func DetermineWalletMainUtxoForPublicKey( + walletPublicKey *ecdsa.PublicKey, + bridgeChain BridgeChain, + btcChain bitcoin.Chain, +) (*bitcoin.UnspentTransactionOutput, error) { + walletPublicKeyHash := bitcoin.PublicKeyHash(walletPublicKey) + + walletScripts, err := walletPublicKeyScripts(walletPublicKey) + if err != nil { + return nil, err + } + + return determineWalletMainUtxo( + walletPublicKeyHash, + walletScripts, + bridgeChain, + btcChain, + ) +} + +func determineWalletMainUtxo( + walletPublicKeyHash [20]byte, + walletScripts []bitcoin.Script, + bridgeChain BridgeChain, + btcChain bitcoin.Chain, ) (*bitcoin.UnspentTransactionOutput, error) { walletChainData, err := bridgeChain.GetWallet(walletPublicKeyHash) if err != nil { @@ -543,20 +1029,15 @@ func DetermineWalletMainUtxo( // fetch full transaction data (time-consuming calls) starting from // the most recent transactions as there is a high chance the main UTXO // comes from there. - txHashes, err := btcChain.GetTxHashesForPublicKeyHash(walletPublicKeyHash) + txHashes, err := getTxHashesForWalletScripts( + btcChain, + walletPublicKeyHash, + walletScripts, + ) if err != nil { return nil, fmt.Errorf("cannot get transactions history for wallet: [%v]", err) } - walletP2PKH, err := bitcoin.PayToPublicKeyHash(walletPublicKeyHash) - if err != nil { - return nil, fmt.Errorf("cannot construct P2PKH for wallet: [%v]", err) - } - walletP2WPKH, err := bitcoin.PayToWitnessPublicKeyHash(walletPublicKeyHash) - if err != nil { - return nil, fmt.Errorf("cannot construct P2WPKH for wallet: [%v]", err) - } - // Start iterating from the latest transaction as the chance it matches // the wallet main UTXO is the highest. for i := len(txHashes) - 1; i >= 0; i-- { @@ -575,8 +1056,7 @@ func DetermineWalletMainUtxo( // the wallet public key hash. for outputIndex, output := range transaction.Outputs { script := output.PublicKeyScript - matchesWallet := bytes.Equal(script, walletP2PKH) || - bytes.Equal(script, walletP2WPKH) + matchesWallet := scriptMatchesAny(script, walletScripts) // Once the right output is found, check whether their hash // matches the main UTXO hash stored on-chain. If so, this @@ -608,11 +1088,63 @@ func EnsureWalletSyncedBetweenChains( walletMainUtxo *bitcoin.UnspentTransactionOutput, bridgeChain BridgeChain, btcChain bitcoin.Chain, +) error { + walletScripts, err := legacyWalletPublicKeyScripts(walletPublicKeyHash) + if err != nil { + return err + } + + return ensureWalletSyncedBetweenChains( + walletPublicKeyHash, + walletScripts, + walletMainUtxo, + bridgeChain, + btcChain, + ) +} + +// EnsureWalletSyncedBetweenChainsForPublicKey makes sure all actions taken by +// the wallet on the Bitcoin chain are reflected in the host chain Bridge. +// Unlike EnsureWalletSyncedBetweenChains, this variant can discover Taproot +// wallet outputs. +func EnsureWalletSyncedBetweenChainsForPublicKey( + walletPublicKey *ecdsa.PublicKey, + walletMainUtxo *bitcoin.UnspentTransactionOutput, + bridgeChain BridgeChain, + btcChain bitcoin.Chain, +) error { + walletPublicKeyHash := bitcoin.PublicKeyHash(walletPublicKey) + + walletScripts, err := walletPublicKeyScripts(walletPublicKey) + if err != nil { + return err + } + + return ensureWalletSyncedBetweenChains( + walletPublicKeyHash, + walletScripts, + walletMainUtxo, + bridgeChain, + btcChain, + ) +} + +func ensureWalletSyncedBetweenChains( + walletPublicKeyHash [20]byte, + walletScripts []bitcoin.Script, + walletMainUtxo *bitcoin.UnspentTransactionOutput, + bridgeChain BridgeChain, + btcChain bitcoin.Chain, ) error { // Take UTXOs controlled by the wallet on Bitcoin chain. Those are outputs // coming from confirmed transactions, ready to be spent right now, and // not used as inputs of other (either confirmed or mempool) transactions. - confirmedUtxos, err := btcChain.GetUtxosForPublicKeyHash(walletPublicKeyHash) + confirmedUtxos, err := getUtxosForWalletScripts( + btcChain, + walletPublicKeyHash, + walletScripts, + true, + ) if err != nil { return fmt.Errorf("cannot get confirmed UTXOs: [%v]", err) } @@ -659,7 +1191,12 @@ func EnsureWalletSyncedBetweenChains( // to the wallet address. We need to look at the confirmed and mempool // UTXOs and make sure there are no transactions produced by the wallet // there. - mempoolUtxos, err := btcChain.GetMempoolUtxosForPublicKeyHash(walletPublicKeyHash) + mempoolUtxos, err := getUtxosForWalletScripts( + btcChain, + walletPublicKeyHash, + walletScripts, + false, + ) if err != nil { return fmt.Errorf("cannot get mempool UTXOs: [%v]", err) } @@ -766,6 +1303,100 @@ func EnsureWalletSyncedBetweenChains( } } +type walletPublicKeyScriptsChain interface { + GetTxHashesForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, + ) ([]bitcoin.Hash, error) + GetUtxosForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, + ) ([]*bitcoin.UnspentTransactionOutput, error) + GetMempoolUtxosForPublicKeyScripts( + publicKeyScripts []bitcoin.Script, + ) ([]*bitcoin.UnspentTransactionOutput, error) +} + +func legacyWalletPublicKeyScripts( + walletPublicKeyHash [20]byte, +) ([]bitcoin.Script, error) { + walletP2PKH, err := bitcoin.PayToPublicKeyHash(walletPublicKeyHash) + if err != nil { + return nil, fmt.Errorf("cannot construct P2PKH for wallet: [%v]", err) + } + + walletP2WPKH, err := bitcoin.PayToWitnessPublicKeyHash(walletPublicKeyHash) + if err != nil { + return nil, fmt.Errorf("cannot construct P2WPKH for wallet: [%v]", err) + } + + return []bitcoin.Script{walletP2PKH, walletP2WPKH}, nil +} + +func walletPublicKeyScripts( + walletPublicKey *ecdsa.PublicKey, +) ([]bitcoin.Script, error) { + walletPublicKeyHash := bitcoin.PublicKeyHash(walletPublicKey) + + walletScripts, err := legacyWalletPublicKeyScripts(walletPublicKeyHash) + if err != nil { + return nil, err + } + + xOnlyPublicKey, err := walletXOnlyPublicKey(walletPublicKey) + if err != nil { + return nil, err + } + + walletP2TR, err := bitcoin.PayToTaproot(xOnlyPublicKey) + if err != nil { + return nil, fmt.Errorf("cannot construct P2TR for wallet: [%v]", err) + } + + return append(walletScripts, walletP2TR), nil +} + +func getTxHashesForWalletScripts( + btcChain bitcoin.Chain, + walletPublicKeyHash [20]byte, + walletScripts []bitcoin.Script, +) ([]bitcoin.Hash, error) { + if scriptChain, ok := btcChain.(walletPublicKeyScriptsChain); ok { + return scriptChain.GetTxHashesForPublicKeyScripts(walletScripts) + } + + return btcChain.GetTxHashesForPublicKeyHash(walletPublicKeyHash) +} + +func getUtxosForWalletScripts( + btcChain bitcoin.Chain, + walletPublicKeyHash [20]byte, + walletScripts []bitcoin.Script, + confirmed bool, +) ([]*bitcoin.UnspentTransactionOutput, error) { + if scriptChain, ok := btcChain.(walletPublicKeyScriptsChain); ok { + if confirmed { + return scriptChain.GetUtxosForPublicKeyScripts(walletScripts) + } + + return scriptChain.GetMempoolUtxosForPublicKeyScripts(walletScripts) + } + + if confirmed { + return btcChain.GetUtxosForPublicKeyHash(walletPublicKeyHash) + } + + return btcChain.GetMempoolUtxosForPublicKeyHash(walletPublicKeyHash) +} + +func scriptMatchesAny(script bitcoin.Script, scripts []bitcoin.Script) bool { + for _, candidate := range scripts { + if bytes.Equal(script, candidate) { + return true + } + } + + return false +} + // signer represents a threshold signer of a tBTC wallet. A signer holds // a wallet tECDSA private key share and is able to participate in the // signing process. @@ -785,6 +1416,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. @@ -793,19 +1428,33 @@ 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: signerMaterial, } } +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]", diff --git a/pkg/tbtc/wallet_id.go b/pkg/tbtc/wallet_id.go new file mode 100644 index 0000000000..6605b762fe --- /dev/null +++ b/pkg/tbtc/wallet_id.go @@ -0,0 +1,30 @@ +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 +} + +// 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_from_signer_default.go b/pkg/tbtc/wallet_id_from_signer_default.go new file mode 100644 index 0000000000..7f945cb208 --- /dev/null +++ b/pkg/tbtc/wallet_id_from_signer_default.go @@ -0,0 +1,12 @@ +//go:build !frost_native + +package tbtc + +import "crypto/ecdsa" + +func calculateWalletIDForSigner( + signer *signer, + calculateLegacyWalletID func(*ecdsa.PublicKey) ([32]byte, error), +) ([32]byte, error) { + return calculateLegacyWalletID(signer.wallet.publicKey) +} diff --git a/pkg/tbtc/wallet_id_from_signer_frost_native.go b/pkg/tbtc/wallet_id_from_signer_frost_native.go new file mode 100644 index 0000000000..e19da61aa6 --- /dev/null +++ b/pkg/tbtc/wallet_id_from_signer_frost_native.go @@ -0,0 +1,103 @@ +//go:build frost_native + +package tbtc + +import ( + "crypto/ecdsa" + "encoding/json" + "fmt" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" +) + +func calculateWalletIDForSigner( + signer *signer, + calculateLegacyWalletID func(*ecdsa.PublicKey) ([32]byte, error), +) ([32]byte, error) { + if signer == nil { + return [32]byte{}, fmt.Errorf("signer is nil") + } + + walletID, isFrostWallet, err := frostWalletIDFromSigner(signer) + if err != nil { + return [32]byte{}, err + } + if isFrostWallet { + return walletID, nil + } + + return calculateLegacyWalletID(signer.wallet.publicKey) +} + +func frostWalletIDFromSigner(signer *signer) ([32]byte, bool, error) { + material, ok := nativeSignerMaterialFromSigner(signer) + if !ok { + return [32]byte{}, false, nil + } + + switch material.Format { + case frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1: + case frostsigning.NativeSignerMaterialFormatFrostUniFFIV2: + return [32]byte{}, false, fmt.Errorf( + "%w: unsupported UniFFI FROST signer material format [%s]; "+ + "it cannot sweep Taproot deposits; use [%s]", + frostsigning.ErrUnsupportedSignerMaterialFormat, + frostsigning.NativeSignerMaterialFormatFrostUniFFIV2, + frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + ) + default: + return [32]byte{}, false, nil + } + + var payload frostsigning.NativeTBTCSignerMaterialPayload + if err := json.Unmarshal(material.Payload, &payload); err != nil { + return [32]byte{}, false, fmt.Errorf( + "cannot decode FrostTBTCSignerV1 signer material: [%w]", + err, + ) + } + + if payload.KeyGroupSource == + frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey { + return [32]byte{}, false, nil + } + + xOnlyOutputKey, err := frostsigning.ExtractTaprootOutputKeyFromMaterial( + material, + ) + if err != nil { + return [32]byte{}, true, err + } + if len(xOnlyOutputKey) != 32 { + return [32]byte{}, true, fmt.Errorf( + "FROST DKG output key length [%d] is not 32", + len(xOnlyOutputKey), + ) + } + + var walletID [32]byte + copy(walletID[:], xOnlyOutputKey) + + return walletID, true, nil +} + +func nativeSignerMaterialFromSigner( + signer *signer, +) (*frostsigning.NativeSignerMaterial, bool) { + if signer == nil { + return nil, false + } + + switch material := signer.signingMaterial().(type) { + case *frostsigning.NativeSignerMaterial: + if material == nil { + return nil, false + } + + return material, true + case frostsigning.NativeSignerMaterial: + return &material, true + default: + return nil, false + } +} diff --git a/pkg/tbtc/wallet_id_from_signer_frost_native_test.go b/pkg/tbtc/wallet_id_from_signer_frost_native_test.go new file mode 100644 index 0000000000..e3d358ae92 --- /dev/null +++ b/pkg/tbtc/wallet_id_from_signer_frost_native_test.go @@ -0,0 +1,150 @@ +//go:build frost_native + +package tbtc + +import ( + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "errors" + "testing" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" +) + +func TestCalculateWalletIDForSigner_FrostUniFFIV2RejectsUnsupportedMaterial(t *testing.T) { + payload, err := json.Marshal(struct { + KeyPackage *frostsigning.NativeFROSTKeyPackage `json:"keyPackage"` + PublicKeyPackage *frostsigning.NativeFROSTPublicKeyPackage `json:"publicKeyPackage"` + }{ + KeyPackage: &frostsigning.NativeFROSTKeyPackage{ + Identifier: "member-1", + Data: []byte{0x01}, + }, + PublicKeyPackage: &frostsigning.NativeFROSTPublicKeyPackage{ + VerifyingKey: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + }) + if err != nil { + t.Fatalf("unexpected payload marshal error: [%v]", err) + } + + signer := createMockSigner(t) + signer.signerMaterial = &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV2, + Payload: payload, + } + + legacyCalculatorCalled := false + _, err = calculateWalletIDForSigner( + signer, + func(_ *ecdsa.PublicKey) ([32]byte, error) { + legacyCalculatorCalled = true + return [32]byte{0xff}, nil + }, + ) + if err == nil { + t.Fatal("expected unsupported material error") + } + if !errors.Is(err, frostsigning.ErrUnsupportedSignerMaterialFormat) { + t.Fatalf( + "unexpected wallet ID calculation error\nexpected: [%v]\nactual: [%v]", + frostsigning.ErrUnsupportedSignerMaterialFormat, + err, + ) + } + if legacyCalculatorCalled { + t.Fatal("legacy wallet ID calculator should not have been called") + } +} + +func TestCalculateWalletIDForSigner_TBTCSignerDkgPersistedUsesXOnlyOutputKey( + t *testing.T, +) { + const xOnlyOutputKey = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + + payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ + KeyGroup: xOnlyOutputKey, + KeyGroupSource: frostsigning.NativeTBTCSignerKeyGroupSourceDKGPersisted, + }) + if err != nil { + t.Fatalf("unexpected payload marshal error: [%v]", err) + } + + signer := createMockSigner(t) + signer.signerMaterial = &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + + legacyCalculatorCalled := false + walletID, err := calculateWalletIDForSigner( + signer, + func(_ *ecdsa.PublicKey) ([32]byte, error) { + legacyCalculatorCalled = true + return [32]byte{0xff}, nil + }, + ) + if err != nil { + t.Fatalf("unexpected wallet ID calculation error: [%v]", err) + } + if legacyCalculatorCalled { + t.Fatal("legacy wallet ID calculator should not have been called") + } + + var expectedWalletID [32]byte + expectedBytes, err := hex.DecodeString(xOnlyOutputKey) + if err != nil { + t.Fatalf("unexpected hex decode error: [%v]", err) + } + copy(expectedWalletID[:], expectedBytes) + + if walletID != expectedWalletID { + t.Fatalf( + "unexpected FROST wallet ID\nexpected: [0x%x]\nactual: [0x%x]", + expectedWalletID, + walletID, + ) + } +} + +func TestCalculateWalletIDForSigner_TBTCSignerLegacyKeyGroupSourceUsesLegacyWalletID( + t *testing.T, +) { + payload, err := json.Marshal(frostsigning.NativeTBTCSignerMaterialPayload{ + KeyGroup: "legacy-key-group", + KeyGroupSource: frostsigning.NativeTBTCSignerKeyGroupSourceLegacyWalletPubKey, + }) + if err != nil { + t.Fatalf("unexpected payload marshal error: [%v]", err) + } + + signer := createMockSigner(t) + signer.signerMaterial = &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostTBTCSignerV1, + Payload: payload, + } + + expectedWalletID := [32]byte{0x11, 0x22, 0x33} + legacyCalculatorCalled := false + walletID, err := calculateWalletIDForSigner( + signer, + func(_ *ecdsa.PublicKey) ([32]byte, error) { + legacyCalculatorCalled = true + return expectedWalletID, nil + }, + ) + if err != nil { + t.Fatalf("unexpected wallet ID calculation error: [%v]", err) + } + if !legacyCalculatorCalled { + t.Fatal("legacy wallet ID calculator was not called") + } + if walletID != expectedWalletID { + t.Fatalf( + "unexpected legacy wallet ID\nexpected: [0x%x]\nactual: [0x%x]", + expectedWalletID, + walletID, + ) + } +} diff --git a/pkg/tbtc/wallet_id_test.go b/pkg/tbtc/wallet_id_test.go new file mode 100644 index 0000000000..eb6ee3688e --- /dev/null +++ b/pkg/tbtc/wallet_id_test.go @@ -0,0 +1,89 @@ +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, + ) + } +} + +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") + } +} 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..c67c72691d --- /dev/null +++ b/pkg/tbtc/wallet_sign_transaction_build_taproot_tx_test.go @@ -0,0 +1,1687 @@ +package tbtc + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "math/big" + "strings" + "testing" + + btcec2 "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "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" +) + +func TestWalletTransactionExecutor_SignTransaction_ReturnsBuildTaprootTxError( + t *testing.T, +) { + privateKey, unsignedTx, _, _ := buildTaprootTxSubstitutionFixture(t) + + original := buildTaprootTxViaNativeSignerFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = original + }) + + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return "", errors.New("build tx failed") + } + + wte := &walletTransactionExecutor{ + executingWallet: wallet{ + publicKey: &privateKey.PublicKey, + }, + signingExecutor: &unexpectedSigningExecutorForBuildTaprootTxError{}, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + logger := &warningCaptureLogger{} + + _, err := wte.signTransaction(logger, unsignedTx, 0, 0) + if err == nil { + t.Fatal("expected signTransaction error") + } + + if !strings.Contains(err.Error(), "native tbtc-signer") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +func TestWalletTransactionExecutor_SignTransaction_PropagatesBuildTaprootTxBridgeOperationError( + t *testing.T, +) { + privateKey, unsignedTx, _, _ := buildTaprootTxSubstitutionFixture(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{ + executingWallet: wallet{ + publicKey: &privateKey.PublicKey, + }, + signingExecutor: &unexpectedSigningExecutorForBuildTaprootTxError{}, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + logger := &warningCaptureLogger{} + + _, err := wte.signTransaction(logger, unsignedTx, 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, +) { + 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.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 999, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + }, + 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( + "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]) + } + + if !strings.Contains(logger.warningMessages[0], "output value mismatch") { + t.Fatalf("missing divergence detail in warning: [%v]", logger.warningMessages[0]) + } +} + +func TestEvaluateNativeUnsignedTransactionForSigning_ObservationalModeLogsWarningOnStructuralDivergence( + 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.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 1000, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + }, + 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( + "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]) + } + + if !strings.Contains(logger.warningMessages[0], "version mismatch") { + t.Fatalf("missing divergence detail in warning: [%v]", logger.warningMessages[0]) + } +} + +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.Transaction{ + Version: 2, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: txHash, + OutputIndex: 7, + }, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 999, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + }, + 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 !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") + } + + 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, + 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{ + { + Value: 1000, + PublicKeyScript: scriptPubKey, + }, + }, + Locktime: 0, + } + + nativeTxHex := hex.EncodeToString(nativeTransaction.Serialize(bitcoin.Standard)) + + expectedTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + 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.Fatal("expected substitution-mode structural divergence error") + } + + if !strings.Contains(err.Error(), "diverges") { + 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") + } + + 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 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) + } + + 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, _ := 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 != 1 { + t.Fatalf( + "unexpected non-substituted transaction version\nexpected: [1]\nactual: [%v]", + tx.Version, + ) + } + + if tx.Locktime != 0 { + t.Fatalf( + "unexpected non-substituted transaction locktime\nexpected: [0]\nactual: [%v]", + tx.Locktime, + ) + } + + if tx.Inputs[0].Sequence != 0xffffffff { + t.Fatalf( + "unexpected non-substituted input sequence\nexpected: [4294967295]\nactual: [%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( + 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 !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) + } +} + +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 !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) + } +} + +func TestWalletTransactionExecutor_SignTransaction_AppliesTaprootKeyPathSignatures( + t *testing.T, +) { + originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = originalBuildTaprootTxViaNativeSignerFn + }) + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return "", nil + } + + privateKeyBytes := mustDecodeHex( + t, + "0101010101010101010101010101010101010101010101010101010101010101", + ) + privateKey, publicKey := btcec2.PrivKeyFromBytes(privateKeyBytes) + + var taprootOutputKey [32]byte + copy(taprootOutputKey[:], schnorr.SerializePubKey(publicKey)) + + inputScript, err := bitcoin.PayToTaproot(taprootOutputKey) + if err != nil { + t.Fatalf("cannot create taproot input script: [%v]", err) + } + + var outputPublicKeyHash [20]byte + copy( + outputPublicKeyHash[:], + mustDecodeHex(t, "0202020202020202020202020202020202020202"), + ) + outputScript, err := bitcoin.PayToWitnessPublicKeyHash(outputPublicKeyHash) + if err != nil { + t.Fatalf("cannot create output script: [%v]", err) + } + + localBitcoinChain := newLocalBitcoinChain() + fundingTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{ + 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, + }, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 100000, + PublicKeyScript: inputScript, + }, + }, + Locktime: 0, + } + if err := localBitcoinChain.BroadcastTransaction(fundingTransaction); err != nil { + t.Fatalf("cannot broadcast funding transaction: [%v]", err) + } + + unsignedTx := bitcoin.NewTransactionBuilder(localBitcoinChain) + if err := unsignedTx.AddTaprootKeyPathInput( + &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTransaction.Hash(), + OutputIndex: 0, + }, + Value: 100000, + }, + ); err != nil { + t.Fatalf("cannot add taproot input: [%v]", err) + } + unsignedTx.AddOutput(&bitcoin.TransactionOutput{ + Value: 90000, + PublicKeyScript: outputScript, + }) + + wte := &walletTransactionExecutor{ + executingWallet: generateWallet(big.NewInt(111)), + signingExecutor: &deterministicSchnorrSigningExecutorForTaproot{ + privateKey: privateKey, + }, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + + tx, err := wte.signTransaction(&warningCaptureLogger{}, unsignedTx, 0, 0) + if err != nil { + t.Fatalf("unexpected signTransaction error: [%v]", err) + } + + expectedSignature := mustDecodeHex( + t, + "5e847a0c22486f3b89ff80edd5afaf4be550aa411a0a7e28cff19d2b5924d77102bbf9a0a51100f4fdfc8435d0e8ff0f61dfdeccd464b78c553b1b4414ac0877", + ) + + if len(tx.Inputs) != 1 { + t.Fatalf("unexpected input count: [%d]", len(tx.Inputs)) + } + if len(tx.Inputs[0].Witness) != 1 { + t.Fatalf("unexpected taproot witness: [%x]", tx.Inputs[0].Witness) + } + if !bytes.Equal(expectedSignature, tx.Inputs[0].Witness[0]) { + t.Fatalf( + "unexpected taproot witness signature\nexpected: [%x]\nactual: [%x]", + expectedSignature, + tx.Inputs[0].Witness[0], + ) + } + if len(tx.Inputs[0].SignatureScript) != 0 { + t.Fatalf( + "unexpected signature script for taproot input: [%x]", + tx.Inputs[0].SignatureScript, + ) + } +} + +func TestWalletTransactionExecutor_SignTransaction_AppliesTweakedTaprootKeyPathSignatures( + t *testing.T, +) { + originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = originalBuildTaprootTxViaNativeSignerFn + }) + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return "", nil + } + + internalPrivateKeyBytes := mustDecodeHex( + t, + "0101010101010101010101010101010101010101010101010101010101010101", + ) + _, internalPublicKey := btcec2.PrivKeyFromBytes(internalPrivateKeyBytes) + + var internalKey [32]byte + copy(internalKey[:], schnorr.SerializePubKey(internalPublicKey)) + + refundLeaf := bitcoin.Script(mustDecodeHex( + t, + "76a9140102030405060708090a0b0c0d0e0f101112131488ac", + )) + merkleRoot, err := bitcoin.TaprootLeafHash(refundLeaf) + if err != nil { + t.Fatalf("cannot compute taproot leaf hash: [%v]", err) + } + + taprootOutputKey, err := bitcoin.TaprootOutputKey(internalKey, &merkleRoot) + if err != nil { + t.Fatalf("cannot derive taproot output key: [%v]", err) + } + + inputScript, err := bitcoin.PayToTaproot(taprootOutputKey) + if err != nil { + t.Fatalf("cannot create taproot input script: [%v]", err) + } + + var outputPublicKeyHash [20]byte + copy( + outputPublicKeyHash[:], + mustDecodeHex(t, "0202020202020202020202020202020202020202"), + ) + outputScript, err := bitcoin.PayToWitnessPublicKeyHash(outputPublicKeyHash) + if err != nil { + t.Fatalf("cannot create output script: [%v]", err) + } + + localBitcoinChain := newLocalBitcoinChain() + fundingTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{0x10}, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 100000, + PublicKeyScript: inputScript, + }, + }, + Locktime: 0, + } + if err := localBitcoinChain.BroadcastTransaction(fundingTransaction); err != nil { + t.Fatalf("cannot broadcast funding transaction: [%v]", err) + } + + unsignedTx := bitcoin.NewTransactionBuilder(localBitcoinChain) + if err := unsignedTx.AddTaprootKeyPathInputWithMerkleRoot( + &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTransaction.Hash(), + OutputIndex: 0, + }, + Value: 100000, + }, + internalKey, + merkleRoot, + ); err != nil { + t.Fatalf("cannot add tweaked taproot input: [%v]", err) + } + unsignedTx.AddOutput(&bitcoin.TransactionOutput{ + Value: 90000, + PublicKeyScript: outputScript, + }) + + tweakedPrivateKeyBytes := mustDecodeHex( + t, + "6ba56a44ff544e35d38fd126659aa68b2c4677a7ebbf7464ad2e9d86c18e1149", + ) + tweakedPrivateKey, _ := btcec2.PrivKeyFromBytes(tweakedPrivateKeyBytes) + + signingExecutor := &taprootMerkleRootRecordingSchnorrSigningExecutor{ + privateKey: tweakedPrivateKey, + } + + wte := &walletTransactionExecutor{ + executingWallet: generateWallet(big.NewInt(111)), + signingExecutor: signingExecutor, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + + tx, err := wte.signTransaction(&warningCaptureLogger{}, unsignedTx, 0, 0) + if err != nil { + t.Fatalf("unexpected signTransaction error: [%v]", err) + } + + if signingExecutor.signBatchCalled { + t.Fatal("ordinary signBatch must not be called for tweaked taproot input") + } + + if len(signingExecutor.taprootMerkleRoots) != 1 { + t.Fatalf( + "unexpected taproot merkle root count\nexpected: [%d]\nactual: [%d]", + 1, + len(signingExecutor.taprootMerkleRoots), + ) + } + if signingExecutor.taprootMerkleRoots[0] == nil { + t.Fatal("expected taproot merkle root") + } + if !bytes.Equal(signingExecutor.taprootMerkleRoots[0][:], merkleRoot[:]) { + t.Fatalf( + "unexpected taproot merkle root\nexpected: [%x]\nactual: [%x]", + merkleRoot, + *signingExecutor.taprootMerkleRoots[0], + ) + } + + if len(tx.Inputs) != 1 { + t.Fatalf("unexpected input count: [%d]", len(tx.Inputs)) + } + if len(tx.Inputs[0].Witness) != 1 { + t.Fatalf("unexpected taproot witness: [%x]", tx.Inputs[0].Witness) + } + if len(tx.Inputs[0].SignatureScript) != 0 { + t.Fatalf( + "unexpected signature script for taproot input: [%x]", + tx.Inputs[0].SignatureScript, + ) + } +} + +func TestWalletTransactionExecutor_SignTransaction_RejectsMixedTaprootAndLegacyInputsBeforeSigning( + t *testing.T, +) { + originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = originalBuildTaprootTxViaNativeSignerFn + }) + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return "", nil + } + + var taprootOutputKey [32]byte + copy( + taprootOutputKey[:], + mustDecodeHex( + t, + "1b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f", + ), + ) + taprootInputScript, err := bitcoin.PayToTaproot(taprootOutputKey) + if err != nil { + t.Fatalf("cannot create taproot input script: [%v]", err) + } + + var witnessPublicKeyHash [20]byte + copy( + witnessPublicKeyHash[:], + mustDecodeHex(t, "0202020202020202020202020202020202020202"), + ) + witnessInputScript, err := bitcoin.PayToWitnessPublicKeyHash( + witnessPublicKeyHash, + ) + if err != nil { + t.Fatalf("cannot create witness input script: [%v]", err) + } + + localBitcoinChain := newLocalBitcoinChain() + taprootFundingTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{0x01}, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 100000, + PublicKeyScript: taprootInputScript, + }, + }, + Locktime: 0, + } + if err := localBitcoinChain.BroadcastTransaction( + taprootFundingTransaction, + ); err != nil { + t.Fatalf("cannot broadcast taproot funding transaction: [%v]", err) + } + + legacyFundingTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{0x02}, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 50000, + PublicKeyScript: witnessInputScript, + }, + }, + Locktime: 0, + } + if err := localBitcoinChain.BroadcastTransaction( + legacyFundingTransaction, + ); err != nil { + t.Fatalf("cannot broadcast legacy funding transaction: [%v]", err) + } + + unsignedTx := bitcoin.NewTransactionBuilder(localBitcoinChain) + if err := unsignedTx.AddTaprootKeyPathInput( + &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: taprootFundingTransaction.Hash(), + OutputIndex: 0, + }, + Value: 100000, + }, + ); err != nil { + t.Fatalf("cannot add taproot input: [%v]", err) + } + if err := unsignedTx.AddPublicKeyHashInput( + &bitcoin.UnspentTransactionOutput{ + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: legacyFundingTransaction.Hash(), + OutputIndex: 0, + }, + Value: 50000, + }, + ); err != nil { + t.Fatalf("cannot add legacy input: [%v]", err) + } + unsignedTx.AddOutput(&bitcoin.TransactionOutput{ + Value: 140000, + PublicKeyScript: witnessInputScript, + }) + + wte := &walletTransactionExecutor{ + executingWallet: generateWallet(big.NewInt(111)), + signingExecutor: &unexpectedSigningExecutorForBuildTaprootTxError{}, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + + tx, err := wte.signTransaction(&warningCaptureLogger{}, unsignedTx, 0, 0) + if err == nil { + t.Fatal("expected mixed taproot and legacy signing error") + } + if tx != nil { + t.Fatal("expected no signed transaction") + } + if !strings.Contains(err.Error(), "mixed taproot and legacy inputs") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +func TestWalletTransactionExecutor_SignTransaction_RejectsSchnorrForLegacyInputsBeforeSigning( + t *testing.T, +) { + originalBuildTaprootTxViaNativeSignerFn := buildTaprootTxViaNativeSignerFn + t.Cleanup(func() { + buildTaprootTxViaNativeSignerFn = originalBuildTaprootTxViaNativeSignerFn + }) + buildTaprootTxViaNativeSignerFn = func( + unsignedTx *bitcoin.TransactionBuilder, + ) (string, error) { + return "", nil + } + + var publicKeyHash [20]byte + copy( + publicKeyHash[:], + mustDecodeHex(t, "0202020202020202020202020202020202020202"), + ) + inputScript, err := bitcoin.PayToWitnessPublicKeyHash(publicKeyHash) + if err != nil { + t.Fatalf("cannot create witness input script: [%v]", err) + } + + localBitcoinChain := newLocalBitcoinChain() + fundingTransaction := &bitcoin.Transaction{ + Version: 1, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: bitcoin.Hash{0x03}, + OutputIndex: 0, + }, + SignatureScript: []byte{0x51}, + Sequence: 0xffffffff, + }, + }, + Outputs: []*bitcoin.TransactionOutput{ + { + Value: 100000, + PublicKeyScript: inputScript, + }, + }, + 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: 100000, + }, + ); err != nil { + t.Fatalf("cannot add legacy input: [%v]", err) + } + unsignedTx.AddOutput(&bitcoin.TransactionOutput{ + Value: 90000, + PublicKeyScript: inputScript, + }) + + wte := &walletTransactionExecutor{ + executingWallet: generateWallet(big.NewInt(111)), + signingExecutor: &deterministicSchnorrSigningExecutorForTaproot{}, + waitForBlockFn: func(ctx context.Context, block uint64) error { + return nil + }, + } + + tx, err := wte.signTransaction(&warningCaptureLogger{}, unsignedTx, 0, 0) + if err == nil { + t.Fatal("expected schnorr non-taproot signing error") + } + if tx != nil { + t.Fatal("expected no signed transaction") + } + if !strings.Contains(err.Error(), "non-taproot transaction inputs") { + t.Fatalf("unexpected error: [%v]", err) + } +} + +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: 1, + Locktime: 0, + Inputs: []*bitcoin.TransactionInput{ + { + Outpoint: &bitcoin.TransactionOutpoint{ + TransactionHash: fundingTransaction.Hash(), + OutputIndex: 0, + }, + Sequence: 0xffffffff, + }, + }, + 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 { + t.Fatalf("cannot decode hex: [%v]", err) + } + + return result +} + +type warningCaptureLogger struct { + warningMessages []string + infoMessages []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{}) { + wcl.infoMessages = append(wcl.infoMessages, fmt.Sprint(args...)) +} + +func (wcl *warningCaptureLogger) Infof(format string, args ...interface{}) { + wcl.infoMessages = append(wcl.infoMessages, fmt.Sprintf(format, args...)) +} + +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...), + ) +} + +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 +} + +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 +} + +type deterministicSchnorrSigningExecutorForTaproot struct { + privateKey *btcec2.PrivateKey +} + +func (dsseft *deterministicSchnorrSigningExecutorForTaproot) signBatch( + ctx context.Context, + messages []*big.Int, + startBlock uint64, +) ([]*frost.Signature, error) { + signatures := make([]*frost.Signature, 0, len(messages)) + + for _, message := range messages { + signature, err := schnorr.Sign( + dsseft.privateKey, + message.FillBytes(make([]byte, 32)), + ) + if err != nil { + return nil, err + } + + serialized := signature.Serialize() + frostSignature := &frost.Signature{} + copy(frostSignature.R[:], serialized[:32]) + copy(frostSignature.S[:], serialized[32:]) + + signatures = append(signatures, frostSignature) + } + + return signatures, nil +} + +func (dsseft *deterministicSchnorrSigningExecutorForTaproot) usesSchnorrSignatures() bool { + return true +} + +type taprootMerkleRootRecordingSchnorrSigningExecutor struct { + privateKey *btcec2.PrivateKey + signBatchCalled bool + taprootMerkleRoots []*[32]byte +} + +func (tmrrsse *taprootMerkleRootRecordingSchnorrSigningExecutor) signBatch( + ctx context.Context, + messages []*big.Int, + startBlock uint64, +) ([]*frost.Signature, error) { + tmrrsse.signBatchCalled = true + return nil, errors.New("unexpected signBatch invocation") +} + +func (tmrrsse *taprootMerkleRootRecordingSchnorrSigningExecutor) signBatchWithTaprootMerkleRoots( + ctx context.Context, + messages []*big.Int, + taprootMerkleRoots []*[32]byte, + startBlock uint64, +) ([]*frost.Signature, error) { + tmrrsse.taprootMerkleRoots = make([]*[32]byte, len(taprootMerkleRoots)) + for i, taprootMerkleRoot := range taprootMerkleRoots { + if taprootMerkleRoot == nil { + continue + } + + tmrrsse.taprootMerkleRoots[i] = new([32]byte) + copy(tmrrsse.taprootMerkleRoots[i][:], taprootMerkleRoot[:]) + } + + signatures := make([]*frost.Signature, 0, len(messages)) + + for _, message := range messages { + signature, err := schnorr.Sign( + tmrrsse.privateKey, + message.FillBytes(make([]byte, 32)), + ) + if err != nil { + return nil, err + } + + serialized := signature.Serialize() + frostSignature := &frost.Signature{} + copy(frostSignature.R[:], serialized[:32]) + copy(frostSignature.S[:], serialized[32:]) + + signatures = append(signatures, frostSignature) + } + + return signatures, nil +} + +func (tmrrsse *taprootMerkleRootRecordingSchnorrSigningExecutor) usesSchnorrSignatures() bool { + return true +} + +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") +} diff --git a/pkg/tbtc/wallet_test.go b/pkg/tbtc/wallet_test.go index 600d75d8b5..28e2f7dc4d 100644 --- a/pkg/tbtc/wallet_test.go +++ b/pkg/tbtc/wallet_test.go @@ -18,6 +18,7 @@ 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" ) @@ -455,12 +456,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), } } @@ -468,7 +469,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() @@ -485,7 +486,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() diff --git a/pkg/tbtc/wallet_utxo_script.go b/pkg/tbtc/wallet_utxo_script.go new file mode 100644 index 0000000000..9f2ce21d4a --- /dev/null +++ b/pkg/tbtc/wallet_utxo_script.go @@ -0,0 +1,48 @@ +package tbtc + +import ( + "fmt" + + "github.com/keep-network/keep-core/pkg/bitcoin" +) + +func walletMainUtxoScriptType( + bitcoinChain bitcoin.Chain, + walletMainUtxo *bitcoin.UnspentTransactionOutput, +) (bitcoin.ScriptType, error) { + if walletMainUtxo == nil { + return bitcoin.NonStandardScript, fmt.Errorf("wallet main UTXO is required") + } + + if walletMainUtxo.Outpoint == nil { + return bitcoin.NonStandardScript, fmt.Errorf( + "wallet main UTXO outpoint is required", + ) + } + + transaction, err := bitcoinChain.GetTransaction( + walletMainUtxo.Outpoint.TransactionHash, + ) + if err != nil { + return bitcoin.NonStandardScript, fmt.Errorf( + "cannot get transaction with hash [%s]: [%v]", + walletMainUtxo.Outpoint.TransactionHash.Hex(bitcoin.InternalByteOrder), + err, + ) + } + + outputIndex := walletMainUtxo.Outpoint.OutputIndex + if outputIndex >= uint32(len(transaction.Outputs)) { + return bitcoin.NonStandardScript, fmt.Errorf( + "output index [%d] out of range for transaction [%s] "+ + "with [%d] outputs", + outputIndex, + walletMainUtxo.Outpoint.TransactionHash.Hex(bitcoin.InternalByteOrder), + len(transaction.Outputs), + ) + } + + return bitcoin.GetScriptType( + transaction.Outputs[outputIndex].PublicKeyScript, + ), nil +} diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index 01e519c462..091f43552e 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -104,6 +104,19 @@ type Chain interface { }, ) error + // ValidateTaprootDepositSweepProposal validates the given Taproot deposit + // sweep proposal against the chain. It requires some additional data about + // the deposits that must be fetched externally. Returns an error if the + // proposal is not valid or nil otherwise. + ValidateTaprootDepositSweepProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.DepositSweepProposal, + depositsExtraInfo []struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, + ) error + // ValidateRedemptionProposal validates the given redemption proposal // against the chain. Returns an error if the proposal is not valid or // nil otherwise. @@ -120,6 +133,11 @@ type Chain interface { AverageBlockTime() time.Duration + // CurrentBlockTimestamp gets the timestamp of the current anchoring chain + // block. Proposal eligibility checks should use this timestamp instead of + // the local process clock because Bridge validators use block.timestamp. + CurrentBlockTimestamp() (time.Time, error) + // GetOperatorID returns the operator ID for the given operator address. GetOperatorID(operatorAddress chain.Address) (chain.OperatorID, error) diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 52f6ef4137..7f444aca31 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -75,6 +75,7 @@ type LocalChain struct { depositRequests map[[32]byte]*tbtc.DepositChainRequest pastDepositRevealedEvents map[[32]byte][]*tbtc.DepositRevealedEvent + pastTaprootDepositRevealedEvents map[[32]byte][]*tbtc.TaprootDepositRevealedEvent pastNewWalletRegisteredEvents map[[32]byte][]*tbtc.NewWalletRegisteredEvent depositParameters depositParameters depositSweepProposalValidations map[[32]byte]bool @@ -98,12 +99,14 @@ type LocalChain struct { operatorIDs map[chain.Address]uint32 redemptionDelays map[[32]byte]time.Duration depositMinAge uint32 + currentBlockTimestamp time.Time } func NewLocalChain() *LocalChain { return &LocalChain{ depositRequests: make(map[[32]byte]*tbtc.DepositChainRequest), pastDepositRevealedEvents: make(map[[32]byte][]*tbtc.DepositRevealedEvent), + pastTaprootDepositRevealedEvents: make(map[[32]byte][]*tbtc.TaprootDepositRevealedEvent), pastNewWalletRegisteredEvents: make(map[[32]byte][]*tbtc.NewWalletRegisteredEvent), depositSweepProposalValidations: make(map[[32]byte]bool), pastRedemptionRequestedEvents: make(map[[32]byte][]*tbtc.RedemptionRequestedEvent), @@ -119,6 +122,7 @@ func NewLocalChain() *LocalChain { movedFundsSweepProposalValidations: make(map[[32]byte]bool), operatorIDs: make(map[chain.Address]uint32), redemptionDelays: make(map[[32]byte]time.Duration), + currentBlockTimestamp: time.Now(), } } @@ -161,6 +165,45 @@ func (lc *LocalChain) AddPastDepositRevealedEvent( return nil } +func (lc *LocalChain) PastTaprootDepositRevealedEvents( + filter *tbtc.DepositRevealedEventFilter, +) ([]*tbtc.TaprootDepositRevealedEvent, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastDepositRevealedEventsKey(filter) + if err != nil { + return nil, err + } + + events, ok := lc.pastTaprootDepositRevealedEvents[eventsKey] + if !ok { + return []*tbtc.TaprootDepositRevealedEvent{}, nil + } + + return events, nil +} + +func (lc *LocalChain) AddPastTaprootDepositRevealedEvent( + filter *tbtc.DepositRevealedEventFilter, + event *tbtc.TaprootDepositRevealedEvent, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastDepositRevealedEventsKey(filter) + if err != nil { + return err + } + + lc.pastTaprootDepositRevealedEvents[eventsKey] = append( + lc.pastTaprootDepositRevealedEvents[eventsKey], + event, + ) + + return nil +} + func buildPastDepositRevealedEventsKey( filter *tbtc.DepositRevealedEventFilter, ) ([32]byte, error) { @@ -615,6 +658,21 @@ func (lc *LocalChain) ValidateDepositSweepProposal( return nil } +func (lc *LocalChain) ValidateTaprootDepositSweepProposal( + walletPublicKeyHash [20]byte, + proposal *tbtc.DepositSweepProposal, + depositsExtraInfo []struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, +) error { + return lc.ValidateDepositSweepProposal( + walletPublicKeyHash, + proposal, + depositsExtraInfo, + ) +} + func (lc *LocalChain) SetDepositSweepProposalValidationResult( walletPublicKeyHash [20]byte, proposal *tbtc.DepositSweepProposal, @@ -985,6 +1043,20 @@ func (lc *LocalChain) AverageBlockTime() time.Duration { return lc.averageBlockTime } +func (lc *LocalChain) CurrentBlockTimestamp() (time.Time, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.currentBlockTimestamp, nil +} + +func (lc *LocalChain) SetCurrentBlockTimestamp(currentBlockTimestamp time.Time) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.currentBlockTimestamp = currentBlockTimestamp +} + func (lc *LocalChain) SetOperatorID( operatorAddress chain.Address, operatorID chain.OperatorID, @@ -1047,6 +1119,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, diff --git a/pkg/tbtcpg/deposit_sweep.go b/pkg/tbtcpg/deposit_sweep.go index 491e4411fa..c3cb873be2 100644 --- a/pkg/tbtcpg/deposit_sweep.go +++ b/pkg/tbtcpg/deposit_sweep.go @@ -114,6 +114,7 @@ type Deposit struct { WalletPublicKeyHash [20]byte DepositKey string IsSwept bool + IsTaproot bool AmountBtc float64 Confirmations uint Vault *chain.Address @@ -147,7 +148,7 @@ func FindDeposits( // deposit-revealed events are queried. func findDeposits( fnLogger log.StandardLogger, - chain Chain, + hostChain Chain, btcChain bitcoin.Chain, walletPublicKeyHash [20]byte, maxNumberOfDeposits int, @@ -157,7 +158,7 @@ func findDeposits( ) ([]*Deposit, error) { fnLogger.Infof("reading revealed deposits from chain") - depositMinAgeSeconds, err := chain.GetDepositMinAge() + depositMinAgeSeconds, err := hostChain.GetDepositMinAge() if err != nil { return nil, fmt.Errorf( "failed to get deposit minimum age: [%w]", @@ -173,7 +174,7 @@ func findDeposits( filter.WalletPublicKeyHash = [][20]byte{walletPublicKeyHash} } - depositRevealedEvents, err := chain.PastDepositRevealedEvents(filter) + depositRevealedEvents, err := hostChain.PastDepositRevealedEvents(filter) if err != nil { return []*Deposit{}, fmt.Errorf( "failed to get past deposit revealed events: [%w]", @@ -181,35 +182,122 @@ func findDeposits( ) } - fnLogger.Infof("found [%d] DepositRevealed events", len(depositRevealedEvents)) + taprootDepositRevealedEvents, err := hostChain.PastTaprootDepositRevealedEvents(filter) + if err != nil { + return []*Deposit{}, fmt.Errorf( + "failed to get past Taproot deposit revealed events: [%w]", + err, + ) + } + + fnLogger.Infof( + "found [%d] DepositRevealed events and [%d] TaprootDepositRevealed events", + len(depositRevealedEvents), + len(taprootDepositRevealedEvents), + ) + + type revealedDepositEvent struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + WalletPublicKeyHash [20]byte + Amount uint64 + Vault *chain.Address + BlockNumber uint64 + IsTaproot bool + } + + revealedDepositEvents := make( + []*revealedDepositEvent, + 0, + len(depositRevealedEvents)+len(taprootDepositRevealedEvents), + ) + + type revealedDepositKey struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + } + + revealedDepositEventsIndex := make(map[revealedDepositKey]int) + appendRevealedDepositEvent := func(event *revealedDepositEvent) { + key := revealedDepositKey{ + FundingTxHash: event.FundingTxHash, + FundingOutputIndex: event.FundingOutputIndex, + } + + if existingIndex, ok := revealedDepositEventsIndex[key]; ok { + // A Taproot reveal may also emit a compatibility DepositRevealed + // event. Keep only the Taproot-specific representation so the + // same deposit cannot appear in both legacy and Taproot sweep groups. + if event.IsTaproot { + revealedDepositEvents[existingIndex] = event + } + + return + } + + revealedDepositEventsIndex[key] = len(revealedDepositEvents) + revealedDepositEvents = append(revealedDepositEvents, event) + } + + for _, event := range depositRevealedEvents { + appendRevealedDepositEvent( + &revealedDepositEvent{ + FundingTxHash: event.FundingTxHash, + FundingOutputIndex: event.FundingOutputIndex, + WalletPublicKeyHash: event.WalletPublicKeyHash, + Amount: event.Amount, + Vault: event.Vault, + BlockNumber: event.BlockNumber, + }, + ) + } + + for _, event := range taprootDepositRevealedEvents { + appendRevealedDepositEvent( + &revealedDepositEvent{ + FundingTxHash: event.FundingTxHash, + FundingOutputIndex: event.FundingOutputIndex, + WalletPublicKeyHash: event.WalletPublicKeyHash, + Amount: event.Amount, + Vault: event.Vault, + BlockNumber: event.BlockNumber, + IsTaproot: true, + }, + ) + } // Take the oldest first - sort.SliceStable(depositRevealedEvents, func(i, j int) bool { - return depositRevealedEvents[i].BlockNumber < depositRevealedEvents[j].BlockNumber + sort.SliceStable(revealedDepositEvents, func(i, j int) bool { + return revealedDepositEvents[i].BlockNumber < revealedDepositEvents[j].BlockNumber }) fnLogger.Infof("getting deposits details") - resultSliceCapacity := len(depositRevealedEvents) + resultSliceCapacity := len(revealedDepositEvents) if maxNumberOfDeposits > 0 { resultSliceCapacity = maxNumberOfDeposits } - // Capture time now for computations. - timeNow := time.Now() + timeNow, err := hostChain.CurrentBlockTimestamp() + if err != nil { + return nil, fmt.Errorf( + "failed to get current block timestamp: [%w]", + err, + ) + } result := make([]*Deposit, 0, resultSliceCapacity) - for _, event := range depositRevealedEvents { + for _, event := range revealedDepositEvents { if len(result) == cap(result) { break } - depositKey := chain.BuildDepositKey(event.FundingTxHash, event.FundingOutputIndex) + depositKey := hostChain.BuildDepositKey(event.FundingTxHash, event.FundingOutputIndex) depositKeyStr := depositKey.Text(16) fnLogger.Debugf("getting details of deposit [%s]", depositKeyStr) - depositRequest, found, err := chain.GetDepositRequest( + depositRequest, found, err := hostChain.GetDepositRequest( event.FundingTxHash, event.FundingOutputIndex, ) @@ -268,6 +356,7 @@ func findDeposits( WalletPublicKeyHash: event.WalletPublicKeyHash, DepositKey: hexutils.Encode(depositKey.Bytes()), IsSwept: isSwept, + IsTaproot: event.IsTaproot, AmountBtc: convertSatToBtc(float64(depositRequest.Amount)), Confirmations: confirmations, Vault: depositRequest.Vault, @@ -351,6 +440,10 @@ func (dst *DepositSweepTask) FindDepositsToSweep( for _, deposit := range unsweptDeposits { var key string var label string + scriptType := "legacy" + if deposit.IsTaproot { + scriptType = "taproot" + } if deposit.Vault == nil { key = "" @@ -359,6 +452,8 @@ func (dst *DepositSweepTask) FindDepositsToSweep( key = strings.ToLower(string(*deposit.Vault)) label = string(*deposit.Vault) } + key = fmt.Sprintf("%s:%s", scriptType, key) + label = fmt.Sprintf("%s, %s", label, scriptType) g, exists := groups[key] if !exists { @@ -413,13 +508,15 @@ func (dst *DepositSweepTask) FindDepositsToSweep( // different vault, making it eligible for a future sweep. // The Warn-level log below flags these deposits for operator // awareness and manual follow-up. - if nilGroup, ok := groups[""]; ok { - for _, deposit := range nilGroup.deposits { - taskLogger.Warnf( - "vault=0x0 deposit [%s] with wallet PKH [0x%x] requires manual follow-up", - deposit.DepositKey, - deposit.WalletPublicKeyHash, - ) + for _, nilGroupKey := range []string{"legacy:", "taproot:"} { + if nilGroup, ok := groups[nilGroupKey]; ok { + for _, deposit := range nilGroup.deposits { + taskLogger.Warnf( + "vault=0x0 deposit [%s] with wallet PKH [0x%x] requires manual follow-up", + deposit.DepositKey, + deposit.WalletPublicKeyHash, + ) + } } } } diff --git a/pkg/tbtcpg/deposit_sweep_test.go b/pkg/tbtcpg/deposit_sweep_test.go index dbfd9e24ea..9873f39bdb 100644 --- a/pkg/tbtcpg/deposit_sweep_test.go +++ b/pkg/tbtcpg/deposit_sweep_test.go @@ -188,6 +188,85 @@ func TestDepositSweepTask_FindDepositsToSweep_UnderflowGuard(t *testing.T) { } } +func TestDepositSweepTask_FindDepositsToSweep_UsesChainTimestamp(t *testing.T) { + currentBlock := uint64(100000) + chainNow := time.Now().Add(3 * time.Hour) + + walletPublicKeyHash := hexToByte20( + "7670343fc00ccc2d0cd65360e6ad400697ea0fed", + ) + + tbtcChain := tbtcpg.NewLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + tbtcChain.SetBlockCounter(blockCounter) + + tbtcChain.SetCurrentBlockTimestamp(chainNow) + tbtcChain.SetDepositMinAge(3600) + + fundingTxHash := hashFromString( + "f7fc639dd598e70a423fd28d39ca2f2d01e93523b847f601b78ac5a2b6da51da", + ) + + tbtcChain.SetDepositRequest( + fundingTxHash, + uint32(1), + &tbtc.DepositChainRequest{ + // This timestamp is deliberately in the future compared to the + // host clock, but mature compared to the chain clock. + RevealedAt: chainNow.Add(-2 * time.Hour), + SweptAt: time.Unix(0, 0), + }, + ) + + btcChain.SetTransaction(fundingTxHash, &bitcoin.Transaction{}) + btcChain.SetTransactionConfirmations( + fundingTxHash, + tbtc.DepositSweepRequiredFundingTxConfirmations, + ) + + err := tbtcChain.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: 0, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + BlockNumber: 90000, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: fundingTxHash, + FundingOutputIndex: 1, + }, + ) + if err != nil { + t.Fatal(err) + } + + task := tbtcpg.NewDepositSweepTask(tbtcChain, btcChain) + + deposits, err := task.FindDepositsToSweep( + &testutils.MockLogger{}, + walletPublicKeyHash, + 5, + ) + if err != nil { + t.Fatal(err) + } + + if len(deposits) != 1 { + t.Fatalf("expected 1 deposit, got %d", len(deposits)) + } + + if deposits[0].FundingTxHash != fundingTxHash { + t.Errorf("unexpected funding tx hash") + } + + if deposits[0].FundingOutputIndex != 1 { + t.Errorf("unexpected funding output index") + } +} + func TestDepositSweepTask_FindDepositsToSweep(t *testing.T) { err := log.SetLogLevel("*", "DEBUG") if err != nil { @@ -555,6 +634,109 @@ func TestFindDepositsToSweep_VaultGrouping(t *testing.T) { } }) + t.Run("taproot compatibility reveals deduplicated before grouping", func(t *testing.T) { + tbtcChain := tbtcpg.NewLocalChain() + btcChain := tbtcpg.NewLocalBitcoinChain() + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + tbtcChain.SetBlockCounter(blockCounter) + tbtcChain.SetDepositMinAge(3600) + + vaultA := chain.Address("0xAA1122BB3344CC5566DD7788EE9900FF00112233") + + // Three ordinary legacy deposits form the largest valid group. + legacyHash1 := setupVaultGroupingDeposit( + t, tbtcChain, btcChain, walletPublicKeyHash, filterStartBlock, + "6611111111111111111111111111111111111111111111111111111111111111", + 0, 290000, &vaultA, + ) + legacyHash2 := setupVaultGroupingDeposit( + t, tbtcChain, btcChain, walletPublicKeyHash, filterStartBlock, + "6622222222222222222222222222222222222222222222222222222222222222", + 0, 290001, &vaultA, + ) + legacyHash3 := setupVaultGroupingDeposit( + t, tbtcChain, btcChain, walletPublicKeyHash, filterStartBlock, + "6633333333333333333333333333333333333333333333333333333333333333", + 0, 290002, &vaultA, + ) + + taprootHash1 := setupVaultGroupingDeposit( + t, tbtcChain, btcChain, walletPublicKeyHash, filterStartBlock, + "6644444444444444444444444444444444444444444444444444444444444444", + 0, 290003, &vaultA, + ) + taprootHash2 := setupVaultGroupingDeposit( + t, tbtcChain, btcChain, walletPublicKeyHash, filterStartBlock, + "6655555555555555555555555555555555555555555555555555555555555555", + 0, 290004, &vaultA, + ) + + for _, taprootDeposit := range []struct { + fundingTxHash bitcoin.Hash + blockNumber uint64 + }{ + {fundingTxHash: taprootHash1, blockNumber: 290003}, + {fundingTxHash: taprootHash2, blockNumber: 290004}, + } { + err := tbtcChain.AddPastTaprootDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: filterStartBlock, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.TaprootDepositRevealedEvent{ + BlockNumber: taprootDeposit.blockNumber, + WalletPublicKeyHash: walletPublicKeyHash, + FundingTxHash: taprootDeposit.fundingTxHash, + FundingOutputIndex: 0, + }, + ) + if err != nil { + t.Fatal(err) + } + } + + task := tbtcpg.NewDepositSweepTask(tbtcChain, btcChain) + deposits, err := task.FindDepositsToSweep( + &testutils.MockLogger{}, + walletPublicKeyHash, + 10, + ) + if err != nil { + t.Fatal(err) + } + + if len(deposits) != 3 { + t.Fatalf("expected 3 legacy deposits, got %d", len(deposits)) + } + + expectedLegacyHashes := map[bitcoin.Hash]bool{ + legacyHash1: true, + legacyHash2: true, + legacyHash3: true, + } + taprootHashes := map[bitcoin.Hash]bool{ + taprootHash1: true, + taprootHash2: true, + } + + for _, deposit := range deposits { + if !expectedLegacyHashes[deposit.FundingTxHash] { + t.Errorf( + "unexpected non-legacy deposit selected: [%v]", + deposit.FundingTxHash, + ) + } + if taprootHashes[deposit.FundingTxHash] { + t.Errorf( + "taproot compatibility reveal selected in legacy group: [%v]", + deposit.FundingTxHash, + ) + } + } + }) + t.Run("mixed vaults minority group excluded", func(t *testing.T) { tbtcChain := tbtcpg.NewLocalChain() btcChain := tbtcpg.NewLocalBitcoinChain() diff --git a/pkg/tbtcpg/internal/test/marshaling.go b/pkg/tbtcpg/internal/test/marshaling.go index 2dd72dbaa0..b44955e5d2 100644 --- a/pkg/tbtcpg/internal/test/marshaling.go +++ b/pkg/tbtcpg/internal/test/marshaling.go @@ -3,14 +3,15 @@ package test import ( "encoding/hex" "encoding/json" + "errors" "fmt" - "github.com/keep-network/keep-core/pkg/tbtcpg" "math/big" "time" "github.com/keep-network/keep-core/internal/hexutils" "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/tbtc" + "github.com/keep-network/keep-core/pkg/tbtcpg" ) // UnmarshalJSON implements a custom JSON unmarshaling logic to produce a @@ -273,7 +274,7 @@ func (psts *ProposeSweepTestScenario) UnmarshalJSON(data []byte) error { // Unmarshal expected error if len(unmarshaled.ExpectedErr) > 0 { - psts.ExpectedErr = fmt.Errorf(unmarshaled.ExpectedErr) + psts.ExpectedErr = errors.New(unmarshaled.ExpectedErr) } return nil diff --git a/pkg/tbtcpg/redemptions.go b/pkg/tbtcpg/redemptions.go index 981e9a8eb7..59dec7a29c 100644 --- a/pkg/tbtcpg/redemptions.go +++ b/pkg/tbtcpg/redemptions.go @@ -383,8 +383,13 @@ redemptionRequestedLoop: }, ) - // Capture time now for computations. - timeNow := time.Now() + timeNow, err := chain.CurrentBlockTimestamp() + if err != nil { + return nil, fmt.Errorf( + "failed to get current block timestamp: [%w]", + err, + ) + } // Only redemption requests in range: // [now - requestTimeout, now - minAge] diff --git a/pkg/tbtcpg/redemptions_test.go b/pkg/tbtcpg/redemptions_test.go index 8f61e2f94b..140a5ae64e 100644 --- a/pkg/tbtcpg/redemptions_test.go +++ b/pkg/tbtcpg/redemptions_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "math/big" "testing" + "time" "github.com/go-test/deep" "github.com/keep-network/keep-core/internal/testutils" @@ -136,6 +137,95 @@ func TestRedemptionAction_FindPendingRedemptions(t *testing.T) { } } +func TestRedemptionAction_FindPendingRedemptions_UsesChainTimestamp(t *testing.T) { + currentBlock := uint64(100000) + averageBlockTime := 10 * time.Second + requestTimeout := uint32(86400) + requestMinAge := uint32(600) + chainNow := time.Now().Add(3 * time.Hour) + + walletPublicKeyHash := hexToByte20( + "7670343fc00ccc2d0cd65360e6ad400697ea0fed", + ) + redeemerOutputScript := bitcoin.Script{ + 0x00, 0x14, 0xe6, 0xf9, 0xd7, 0x47, 0x26, 0xb1, 0x9b, 0x75, + 0xf1, 0x6f, 0xe1, 0xe9, 0xfe, 0xae, 0xc0, 0x48, 0xaa, 0x4f, + 0xa1, 0xd0, + } + + tbtcChain := tbtcpg.NewLocalChain() + tbtcChain.SetAverageBlockTime(averageBlockTime) + tbtcChain.SetCurrentBlockTimestamp(chainNow) + + blockCounter := tbtcpg.NewMockBlockCounter() + blockCounter.SetCurrentBlock(currentBlock) + tbtcChain.SetBlockCounter(blockCounter) + + tbtcChain.SetRedemptionParameters( + 0, + 0, + 0, + 0, + requestTimeout, + nil, + 0, + ) + tbtcChain.SetRedemptionRequestMinAge(requestMinAge) + + requestTimeoutBlocks := uint64(requestTimeout) / + uint64(averageBlockTime.Seconds()) + + err := tbtcChain.AddPastRedemptionRequestedEvent( + &tbtc.RedemptionRequestedEventFilter{ + StartBlock: currentBlock - requestTimeoutBlocks - 1000, + WalletPublicKeyHash: [][20]byte{walletPublicKeyHash}, + }, + &tbtc.RedemptionRequestedEvent{ + WalletPublicKeyHash: walletPublicKeyHash, + RedeemerOutputScript: redeemerOutputScript, + RequestedAmount: 100000, + BlockNumber: 90000, + }, + ) + if err != nil { + t.Fatal(err) + } + + tbtcChain.SetPendingRedemptionRequest( + walletPublicKeyHash, + &tbtc.RedemptionRequest{ + RedeemerOutputScript: redeemerOutputScript, + RequestedAmount: 100000, + // This timestamp is deliberately in the future compared to the + // host clock, but mature compared to the chain clock. + RequestedAt: chainNow.Add(-(time.Duration(requestMinAge) + 1) * time.Second), + }, + ) + tbtcChain.SetRedemptionDelay( + walletPublicKeyHash, + redeemerOutputScript, + 0, + ) + + task := tbtcpg.NewRedemptionTask(tbtcChain, nil) + + redeemersOutputScripts, err := task.FindPendingRedemptions( + &testutils.MockLogger{}, + walletPublicKeyHash, + 1, + ) + if err != nil { + t.Fatal(err) + } + + if diff := deep.Equal( + []bitcoin.Script{redeemerOutputScript}, + redeemersOutputScripts, + ); diff != nil { + t.Errorf("invalid wallets pending redemptions: %v", diff) + } +} + func TestRedemptionAction_ProposeRedemption(t *testing.T) { fromHex := func(hexString string) []byte { bytes, err := hex.DecodeString(hexString) diff --git a/pkg/tecdsa/retry/retry_test.go b/pkg/tecdsa/retry/retry_test.go index af394920a2..3f6c98d260 100644 --- a/pkg/tecdsa/retry/retry_test.go +++ b/pkg/tecdsa/retry/retry_test.go @@ -119,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 TestExcludeOperatorTripletsCountsRightOperatorSeats(t *testing.T) { leftOperator := chain.Address("operator-left") middleOperator := chain.Address("operator-middle")