From 71b524d332873627474cbfbdc1965ad076a0c6ee Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 25 May 2026 07:03:05 -0500 Subject: [PATCH 1/6] feat(frost): implement DKG coordinator --- cmd/flags.go | 2 + cmd/helpers.go | 2 + config/config_test.go | 21 + config/contracts.go | 11 + .../frost/gen/abi/FrostWalletRegistry.go | 1333 +++++++++++++++++ pkg/chain/ethereum/frost/gen/gen.go | 13 + .../gen/validatorabi/FrostDkgValidator.go | 223 +++ pkg/chain/ethereum/frost_dkg.go | 714 +++++++++ pkg/chain/ethereum/tbtc.go | 137 ++ pkg/frost/registry/dkg_result.go | 253 ++++ pkg/frost/registry/dkg_result_test.go | 247 +++ .../registry/testdata/v4_digest_fixture.json | 14 + .../native_frost_dkg_engine_default.go | 17 + .../native_frost_dkg_engine_frost_native.go | 143 ++ ...tive_frost_dkg_engine_frost_native_test.go | 353 +++++ ...ve_frost_dkg_engine_uniffi_frost_native.go | 157 ++ .../native_frost_dkg_protocol_frost_native.go | 689 +++++++++ pkg/tbtc/frost_dkg_chain.go | 86 ++ pkg/tbtc/frost_dkg_coordinator.go | 395 +++++ pkg/tbtc/frost_dkg_execution_default.go | 26 + pkg/tbtc/frost_dkg_execution_frost_native.go | 435 ++++++ pkg/tbtc/frost_dkg_result_signing.go | 271 ++++ pkg/tbtc/registry.go | 10 +- pkg/tbtc/tbtc.go | 4 + pkg/tbtc/wallet_id_from_signer_default.go | 12 + .../wallet_id_from_signer_frost_native.go | 79 + ...wallet_id_from_signer_frost_native_test.go | 63 + 27 files changed, 5708 insertions(+), 2 deletions(-) create mode 100644 pkg/chain/ethereum/frost/gen/abi/FrostWalletRegistry.go create mode 100644 pkg/chain/ethereum/frost/gen/gen.go create mode 100644 pkg/chain/ethereum/frost/gen/validatorabi/FrostDkgValidator.go create mode 100644 pkg/chain/ethereum/frost_dkg.go create mode 100644 pkg/frost/registry/dkg_result.go create mode 100644 pkg/frost/registry/dkg_result_test.go create mode 100644 pkg/frost/registry/testdata/v4_digest_fixture.json create mode 100644 pkg/frost/signing/native_frost_dkg_engine_default.go create mode 100644 pkg/frost/signing/native_frost_dkg_engine_frost_native.go create mode 100644 pkg/frost/signing/native_frost_dkg_engine_frost_native_test.go create mode 100644 pkg/frost/signing/native_frost_dkg_engine_uniffi_frost_native.go create mode 100644 pkg/frost/signing/native_frost_dkg_protocol_frost_native.go create mode 100644 pkg/tbtc/frost_dkg_chain.go create mode 100644 pkg/tbtc/frost_dkg_coordinator.go create mode 100644 pkg/tbtc/frost_dkg_execution_default.go create mode 100644 pkg/tbtc/frost_dkg_execution_frost_native.go create mode 100644 pkg/tbtc/frost_dkg_result_signing.go create mode 100644 pkg/tbtc/wallet_id_from_signer_default.go create mode 100644 pkg/tbtc/wallet_id_from_signer_frost_native.go create mode 100644 pkg/tbtc/wallet_id_from_signer_frost_native_test.go diff --git a/cmd/flags.go b/cmd/flags.go index 097a673466..9e2c43d533 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -394,5 +394,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/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/pkg/chain/ethereum/frost/gen/abi/FrostWalletRegistry.go b/pkg/chain/ethereum/frost/gen/abi/FrostWalletRegistry.go new file mode 100644 index 0000000000..fb4de202e2 --- /dev/null +++ b/pkg/chain/ethereum/frost/gen/abi/FrostWalletRegistry.go @@ -0,0 +1,1333 @@ +// 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 +) + +// Struct0 is an auto generated low-level Go binding around an user-defined struct. +type Struct0 struct { + SubmitterMemberIndex *big.Int + XOnlyOutputKey [32]byte + MembersHash [32]byte + MisbehavedMembersIndices []uint8 + Signatures []byte + SigningMembersIndices []*big.Int + Members []uint32 +} + +// Struct1 is an auto generated low-level Go binding around an user-defined struct. +type Struct1 struct { + SeedTimeout *big.Int + ResultChallengePeriodLength *big.Int + ResultChallengeExtraGas *big.Int + ResultSubmissionTimeout *big.Int + SubmitterPrecedencePeriodLength *big.Int +} + +// FrostWalletRegistryMetaData contains all meta data concerning the FrostWalletRegistry contract. +var FrostWalletRegistryMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"event\",\"name\":\"DkgStarted\",\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"seed\",\"type\":\"uint256\"}]},{\"type\":\"event\",\"name\":\"DkgResultSubmitted\",\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"resultHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"seed\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"result\",\"type\":\"tuple\",\"components\":[{\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"name\":\"membersHash\",\"type\":\"bytes32\"},{\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"name\":\"signatures\",\"type\":\"bytes\"},{\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"name\":\"members\",\"type\":\"uint32[]\"}]}]},{\"type\":\"event\",\"name\":\"DkgResultApproved\",\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"resultHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"approver\",\"type\":\"address\"}]},{\"type\":\"event\",\"name\":\"DkgResultChallenged\",\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"resultHash\",\"type\":\"bytes32\"},{\"indexed\":true,\"name\":\"challenger\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"reason\",\"type\":\"string\"}]},{\"type\":\"event\",\"name\":\"DkgTimedOut\",\"anonymous\":false,\"inputs\":[]},{\"type\":\"event\",\"name\":\"DkgSeedTimedOut\",\"anonymous\":false,\"inputs\":[]},{\"type\":\"function\",\"name\":\"submitDkgResult\",\"stateMutability\":\"nonpayable\",\"inputs\":[{\"name\":\"dkgResult\",\"type\":\"tuple\",\"components\":[{\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"name\":\"membersHash\",\"type\":\"bytes32\"},{\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"name\":\"signatures\",\"type\":\"bytes\"},{\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"name\":\"members\",\"type\":\"uint32[]\"}]}],\"outputs\":[]},{\"type\":\"function\",\"name\":\"approveDkgResult\",\"stateMutability\":\"nonpayable\",\"inputs\":[{\"name\":\"dkgResult\",\"type\":\"tuple\",\"components\":[{\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"name\":\"membersHash\",\"type\":\"bytes32\"},{\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"name\":\"signatures\",\"type\":\"bytes\"},{\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"name\":\"members\",\"type\":\"uint32[]\"}]}],\"outputs\":[]},{\"type\":\"function\",\"name\":\"challengeDkgResult\",\"stateMutability\":\"nonpayable\",\"inputs\":[{\"name\":\"dkgResult\",\"type\":\"tuple\",\"components\":[{\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"name\":\"membersHash\",\"type\":\"bytes32\"},{\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"name\":\"signatures\",\"type\":\"bytes\"},{\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"name\":\"members\",\"type\":\"uint32[]\"}]}],\"outputs\":[]},{\"type\":\"function\",\"name\":\"notifyDkgTimeout\",\"stateMutability\":\"nonpayable\",\"inputs\":[],\"outputs\":[]},{\"type\":\"function\",\"name\":\"notifySeedTimeout\",\"stateMutability\":\"nonpayable\",\"inputs\":[],\"outputs\":[]},{\"type\":\"function\",\"name\":\"isDkgResultValid\",\"stateMutability\":\"view\",\"inputs\":[{\"name\":\"result\",\"type\":\"tuple\",\"components\":[{\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"name\":\"membersHash\",\"type\":\"bytes32\"},{\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"name\":\"signatures\",\"type\":\"bytes\"},{\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"name\":\"members\",\"type\":\"uint32[]\"}]}],\"outputs\":[{\"name\":\"\",\"type\":\"bool\"},{\"name\":\"\",\"type\":\"string\"}]},{\"type\":\"function\",\"name\":\"getWalletCreationState\",\"stateMutability\":\"view\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint8\"}]},{\"type\":\"function\",\"name\":\"selectGroup\",\"stateMutability\":\"view\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint32[]\"}]},{\"type\":\"function\",\"name\":\"sortitionPool\",\"stateMutability\":\"view\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\"}]},{\"type\":\"function\",\"name\":\"dkgParameters\",\"stateMutability\":\"view\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"tuple\",\"components\":[{\"name\":\"seedTimeout\",\"type\":\"uint256\"},{\"name\":\"resultChallengePeriodLength\",\"type\":\"uint256\"},{\"name\":\"resultChallengeExtraGas\",\"type\":\"uint256\"},{\"name\":\"resultSubmissionTimeout\",\"type\":\"uint256\"},{\"name\":\"submitterPrecedencePeriodLength\",\"type\":\"uint256\"}]}]}]", +} + +// 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...) +} + +// 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) (Struct1, error) { + var out []interface{} + err := _FrostWalletRegistry.contract.Call(opts, &out, "dkgParameters") + + if err != nil { + return *new(Struct1), err + } + + out0 := *abi.ConvertType(out[0], new(Struct1)).(*Struct1) + + 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() (Struct1, 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() (Struct1, error) { + return _FrostWalletRegistry.Contract.DkgParameters(&_FrostWalletRegistry.CallOpts) +} + +// 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) +} + +// IsDkgResultValid is a free data retrieval call binding the contract method 0x2fd29068. +// +// Solidity: function isDkgResultValid((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) result) view returns(bool, string) +func (_FrostWalletRegistry *FrostWalletRegistryCaller) IsDkgResultValid(opts *bind.CallOpts, result Struct0) (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 0x2fd29068. +// +// Solidity: function isDkgResultValid((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) result) view returns(bool, string) +func (_FrostWalletRegistry *FrostWalletRegistrySession) IsDkgResultValid(result Struct0) (bool, string, error) { + return _FrostWalletRegistry.Contract.IsDkgResultValid(&_FrostWalletRegistry.CallOpts, result) +} + +// IsDkgResultValid is a free data retrieval call binding the contract method 0x2fd29068. +// +// Solidity: function isDkgResultValid((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) result) view returns(bool, string) +func (_FrostWalletRegistry *FrostWalletRegistryCallerSession) IsDkgResultValid(result Struct0) (bool, string, error) { + return _FrostWalletRegistry.Contract.IsDkgResultValid(&_FrostWalletRegistry.CallOpts, result) +} + +// 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) +} + +// 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) +} + +// ApproveDkgResult is a paid mutator transaction binding the contract method 0x65b514e2. +// +// Solidity: function approveDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) ApproveDkgResult(opts *bind.TransactOpts, dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "approveDkgResult", dkgResult) +} + +// ApproveDkgResult is a paid mutator transaction binding the contract method 0x65b514e2. +// +// Solidity: function approveDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) ApproveDkgResult(dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ApproveDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// ApproveDkgResult is a paid mutator transaction binding the contract method 0x65b514e2. +// +// Solidity: function approveDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) ApproveDkgResult(dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ApproveDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// ChallengeDkgResult is a paid mutator transaction binding the contract method 0xf10f610b. +// +// Solidity: function challengeDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) ChallengeDkgResult(opts *bind.TransactOpts, dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "challengeDkgResult", dkgResult) +} + +// ChallengeDkgResult is a paid mutator transaction binding the contract method 0xf10f610b. +// +// Solidity: function challengeDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) ChallengeDkgResult(dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ChallengeDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// ChallengeDkgResult is a paid mutator transaction binding the contract method 0xf10f610b. +// +// Solidity: function challengeDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) ChallengeDkgResult(dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.ChallengeDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// 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) +} + +// 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) +} + +// SubmitDkgResult is a paid mutator transaction binding the contract method 0xd776003c. +// +// Solidity: function submitDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactor) SubmitDkgResult(opts *bind.TransactOpts, dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.contract.Transact(opts, "submitDkgResult", dkgResult) +} + +// SubmitDkgResult is a paid mutator transaction binding the contract method 0xd776003c. +// +// Solidity: function submitDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistrySession) SubmitDkgResult(dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.SubmitDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// SubmitDkgResult is a paid mutator transaction binding the contract method 0xd776003c. +// +// Solidity: function submitDkgResult((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) dkgResult) returns() +func (_FrostWalletRegistry *FrostWalletRegistryTransactorSession) SubmitDkgResult(dkgResult Struct0) (*types.Transaction, error) { + return _FrostWalletRegistry.Contract.SubmitDkgResult(&_FrostWalletRegistry.TransactOpts, dkgResult) +} + +// 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 Struct0 + Raw types.Log // Blockchain specific contextual infos +} + +// FilterDkgResultSubmitted is a free log retrieval operation binding the contract event 0x4384430e6f3647db226a1f2644148e4c22a002f0e84329434dab4a0f5d5b59aa. +// +// Solidity: event DkgResultSubmitted(bytes32 indexed resultHash, uint256 indexed seed, (uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) 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 0x4384430e6f3647db226a1f2644148e4c22a002f0e84329434dab4a0f5d5b59aa. +// +// Solidity: event DkgResultSubmitted(bytes32 indexed resultHash, uint256 indexed seed, (uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) 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 0x4384430e6f3647db226a1f2644148e4c22a002f0e84329434dab4a0f5d5b59aa. +// +// Solidity: event DkgResultSubmitted(bytes32 indexed resultHash, uint256 indexed seed, (uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) 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 +} + +// 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 +} 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..8adb0c81fd --- /dev/null +++ b/pkg/chain/ethereum/frost/gen/validatorabi/FrostDkgValidator.go @@ -0,0 +1,223 @@ +// 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 +) + +// Struct0 is an auto generated low-level Go binding around an user-defined struct. +type Struct0 struct { + SubmitterMemberIndex *big.Int + XOnlyOutputKey [32]byte + MembersHash [32]byte + MisbehavedMembersIndices []uint8 + Signatures []byte + SigningMembersIndices []*big.Int + Members []uint32 +} + +// FrostDkgValidatorMetaData contains all meta data concerning the FrostDkgValidator contract. +var FrostDkgValidatorMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"function\",\"name\":\"resultDigest\",\"stateMutability\":\"view\",\"inputs\":[{\"name\":\"result\",\"type\":\"tuple\",\"components\":[{\"name\":\"submitterMemberIndex\",\"type\":\"uint256\"},{\"name\":\"xOnlyOutputKey\",\"type\":\"bytes32\"},{\"name\":\"membersHash\",\"type\":\"bytes32\"},{\"name\":\"misbehavedMembersIndices\",\"type\":\"uint8[]\"},{\"name\":\"signatures\",\"type\":\"bytes\"},{\"name\":\"signingMembersIndices\",\"type\":\"uint256[]\"},{\"name\":\"members\",\"type\":\"uint32[]\"}]},{\"name\":\"seed\",\"type\":\"uint256\"},{\"name\":\"bridge\",\"type\":\"address\"},{\"name\":\"registry\",\"type\":\"address\"}],\"outputs\":[{\"name\":\"digest\",\"type\":\"bytes32\"}]}]", +} + +// 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...) +} + +// ResultDigest is a free data retrieval call binding the contract method 0x4669f2d6. +// +// Solidity: function resultDigest((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) result, uint256 seed, address bridge, address registry) view returns(bytes32 digest) +func (_FrostDkgValidator *FrostDkgValidatorCaller) ResultDigest(opts *bind.CallOpts, result Struct0, 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 0x4669f2d6. +// +// Solidity: function resultDigest((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) result, uint256 seed, address bridge, address registry) view returns(bytes32 digest) +func (_FrostDkgValidator *FrostDkgValidatorSession) ResultDigest(result Struct0, 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 0x4669f2d6. +// +// Solidity: function resultDigest((uint256,bytes32,bytes32,uint8[],bytes,uint256[],uint32[]) result, uint256 seed, address bridge, address registry) view returns(bytes32 digest) +func (_FrostDkgValidator *FrostDkgValidatorCallerSession) ResultDigest(result Struct0, seed *big.Int, bridge common.Address, registry common.Address) ([32]byte, error) { + return _FrostDkgValidator.Contract.ResultDigest(&_FrostDkgValidator.CallOpts, result, seed, bridge, registry) +} diff --git a/pkg/chain/ethereum/frost_dkg.go b/pkg/chain/ethereum/frost_dkg.go new file mode 100644 index 0000000000..43b5a20849 --- /dev/null +++ b/pkg/chain/ethereum/frost_dkg.go @@ -0,0 +1,714 @@ +package ethereum + +import ( + "context" + "fmt" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/core/types" + "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" +) + +// 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()) + sink := make(chan *frostabi.FrostWalletRegistryDkgStarted) + + sub, err := tc.frostWalletRegistry.WatchDkgStarted( + &bind.WatchOpts{Context: ctx}, + sink, + nil, + ) + if err != nil { + cancelCtx() + logger.Errorf("failed to watch FROST DKG started 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 started subscription error: [%v]", err) + case event, ok := <-sink: + if !ok { + return + } + handler(&tbtc.FrostDKGStartedEvent{ + Seed: event.Seed, + BlockNumber: event.Raw.BlockNumber, + }) + } + } + }() + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +// PastFrostDKGStartedEvents fetches past FROST DKG started events. +func (tc *TbtcChain) PastFrostDKGStartedEvents( + filter *tbtc.DKGStartedEventFilter, +) ([]*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()) + sink := make(chan *frostabi.FrostWalletRegistryDkgResultSubmitted) + + sub, err := tc.frostWalletRegistry.WatchDkgResultSubmitted( + &bind.WatchOpts{Context: ctx}, + sink, + nil, + nil, + ) + if err != nil { + cancelCtx() + logger.Errorf("failed to watch FROST DKG result submitted 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 result submitted subscription error: [%v]", err) + case event, ok := <-sink: + if !ok { + return + } + result, err := convertFrostDKGResultFromABI(event.Result) + if err != nil { + logger.Errorf("unexpected FROST DKG result in event: [%v]", err) + continue + } + + handler(&tbtc.FrostDKGResultSubmittedEvent{ + Seed: event.Seed, + ResultHash: event.ResultHash, + Result: result, + BlockNumber: event.Raw.BlockNumber, + }) + } + } + }() + + return subscription.NewEventSubscription(func() { + sub.Unsubscribe() + cancelCtx() + }) +} + +// PastFrostDKGResultSubmittedEvents fetches past FROST DKG submitted events. +func (tc *TbtcChain) PastFrostDKGResultSubmittedEvents( + filter *tbtc.DKGStartedEventFilter, +) ([]*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 + 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 +} + +// 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.Struct0, +) (*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.Struct0, error) { + if result == nil { + return frostabi.Struct0{}, 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.Struct0{ + SubmitterMemberIndex: new(big.Int).SetUint64(result.SubmitterMemberIndex), + XOnlyOutputKey: [32]byte(result.XOnlyOutputKey), + MembersHash: result.MembersHash, + MisbehavedMembersIndices: append([]uint8{}, result.MisbehavedMembersIndices...), + Signatures: append([]byte{}, result.Signatures...), + SigningMembersIndices: signingMembersIndices, + Members: append([]uint32{}, result.Members...), + }, nil +} + +func convertFrostDKGResultToValidatorABI( + result *frostregistry.Result, +) (frostvalidatorabi.Struct0, error) { + if result == nil { + return frostvalidatorabi.Struct0{}, 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.Struct0{ + SubmitterMemberIndex: new(big.Int).SetUint64(result.SubmitterMemberIndex), + XOnlyOutputKey: [32]byte(result.XOnlyOutputKey), + MembersHash: result.MembersHash, + MisbehavedMembersIndices: append([]uint8{}, result.MisbehavedMembersIndices...), + Signatures: append([]byte{}, result.Signatures...), + SigningMembersIndices: signingMembersIndices, + Members: append([]uint32{}, result.Members...), + }, 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/tbtc.go b/pkg/chain/ethereum/tbtc.go index 33f65e63be..8af75a88ad 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -13,6 +13,7 @@ import ( "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/crypto" "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" @@ -22,6 +23,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 +41,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" @@ -52,9 +57,14 @@ 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 +185,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 +264,129 @@ 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 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) { diff --git a/pkg/frost/registry/dkg_result.go b/pkg/frost/registry/dkg_result.go new file mode 100644 index 0000000000..b0e2eec490 --- /dev/null +++ b/pkg/frost/registry/dkg_result.go @@ -0,0 +1,253 @@ +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 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..93d0e71fb0 --- /dev/null +++ b/pkg/frost/registry/dkg_result_test.go @@ -0,0 +1,247 @@ +package registry + +import ( + "encoding/hex" + "encoding/json" + "math/big" + "os" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/keep-network/keep-core/pkg/frost" +) + +type v4DigestFixture struct { + 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"` +} + +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 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 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("testdata/v4_digest_fixture.json") + 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/v4_digest_fixture.json b/pkg/frost/registry/testdata/v4_digest_fixture.json new file mode 100644 index 0000000000..2bc7662088 --- /dev/null +++ b/pkg/frost/registry/testdata/v4_digest_fixture.json @@ -0,0 +1,14 @@ +{ + "description": "FROST DKG resultDigest v4 fixture generated from the tBTC TypeScript reference flow.", + "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" +} diff --git a/pkg/frost/signing/native_frost_dkg_engine_default.go b/pkg/frost/signing/native_frost_dkg_engine_default.go new file mode 100644 index 0000000000..0f82118137 --- /dev/null +++ b/pkg/frost/signing/native_frost_dkg_engine_default.go @@ -0,0 +1,17 @@ +//go:build !frost_native + +package signing + +import "fmt" + +// NativeFROSTDKGEngine is unavailable in non-frost_native builds. +type NativeFROSTDKGEngine interface{} + +// RegisterNativeFROSTDKGEngine fails closed when native FROST DKG is not linked +// into the current build. +func RegisterNativeFROSTDKGEngine(engine NativeFROSTDKGEngine) error { + return fmt.Errorf("native FROST DKG engine is unavailable in this build") +} + +// UnregisterNativeFROSTDKGEngine is a no-op in non-frost_native builds. +func UnregisterNativeFROSTDKGEngine() {} 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..658426c7ca --- /dev/null +++ b/pkg/frost/signing/native_frost_dkg_engine_frost_native.go @@ -0,0 +1,143 @@ +//go:build frost_native + +package signing + +import ( + "encoding/json" + "fmt" +) + +// 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. UniFFI Part3 needs to key round-two packages by the 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"` +} + +// SignerMaterial converts the DKG output into the existing FrostUniFFIV2 +// signer-material envelope used by native FROST signing. +func (nfdkg *NativeFROSTDKGResult) SignerMaterial() (*NativeSignerMaterial, error) { + if nfdkg == nil { + return nil, fmt.Errorf("native FROST DKG result is nil") + } + + material := &nativeFROSTUniFFIV2SignerMaterial{ + KeyPackage: nfdkg.KeyPackage, + PublicKeyPackage: nfdkg.PublicKeyPackage, + } + if err := material.validate(); err != nil { + return nil, err + } + + payload, err := json.Marshal(material) + if err != nil { + return nil, fmt.Errorf("cannot marshal native FROST DKG signer material: [%w]", err) + } + + return &NativeSignerMaterial{ + Format: NativeSignerMaterialFormatFrostUniFFIV2, + Payload: payload, + }, nil +} + +// NativeFROSTDKGEngine executes the cryptographic primitives for the three +// FROST DKG parts. It intentionally exposes only serializable package data to +// the coordinator; the bridge implementation is responsible for adapting these +// values to the underlying UniFFI/tbtc-signer handle model. +type NativeFROSTDKGEngine interface { + Part1( + participantIdentifier string, + maxSigners uint16, + minSigners uint16, + ) (*NativeFROSTDKGPart1Result, error) + Part2( + secretPackage *NativeFROSTDKGRound1SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + ) (*NativeFROSTDKGPart2Result, error) + Part3( + secretPackage *NativeFROSTDKGRound2SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + round2Packages []*NativeFROSTDKGRound2Package, + ) (*NativeFROSTDKGResult, error) +} + +var nativeFROSTDKGEngine NativeFROSTDKGEngine + +// RegisterNativeFROSTDKGEngine registers the native FROST DKG cryptographic +// engine used by the FROST wallet-registry coordinator. +func RegisterNativeFROSTDKGEngine(engine NativeFROSTDKGEngine) error { + if engine == nil { + return fmt.Errorf("native FROST DKG engine is nil") + } + + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeFROSTDKGEngine = engine + + return nil +} + +// UnregisterNativeFROSTDKGEngine clears native FROST DKG engine registration. +func UnregisterNativeFROSTDKGEngine() { + executionBackendMutex.Lock() + defer executionBackendMutex.Unlock() + + nativeFROSTDKGEngine = nil +} + +func currentNativeFROSTDKGEngine() NativeFROSTDKGEngine { + executionBackendMutex.RLock() + defer executionBackendMutex.RUnlock() + + return nativeFROSTDKGEngine +} + +// CurrentNativeFROSTDKGEngine returns the registered native FROST DKG engine. +func CurrentNativeFROSTDKGEngine() NativeFROSTDKGEngine { + return currentNativeFROSTDKGEngine() +} diff --git a/pkg/frost/signing/native_frost_dkg_engine_frost_native_test.go b/pkg/frost/signing/native_frost_dkg_engine_frost_native_test.go new file mode 100644 index 0000000000..cc3646df6e --- /dev/null +++ b/pkg/frost/signing/native_frost_dkg_engine_frost_native_test.go @@ -0,0 +1,353 @@ +//go:build frost_native + +package signing + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/keep-network/keep-core/pkg/net/local" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +type mockNativeFROSTDKGEngine struct{} + +func (mnfdkg *mockNativeFROSTDKGEngine) Part1( + participantIdentifier string, + maxSigners uint16, + minSigners uint16, +) (*NativeFROSTDKGPart1Result, error) { + return nil, nil +} + +type deterministicNativeFROSTDKGEngine struct{} + +func (dnfdkg *deterministicNativeFROSTDKGEngine) Part1( + participantIdentifier string, + maxSigners uint16, + minSigners uint16, +) (*NativeFROSTDKGPart1Result, error) { + return &NativeFROSTDKGPart1Result{ + SecretPackage: &NativeFROSTDKGRound1SecretPackage{ + Data: []byte("round1-secret-" + participantIdentifier), + }, + Package: &NativeFROSTDKGRound1Package{ + Identifier: participantIdentifier, + Data: []byte(fmt.Sprintf( + "round1-package-%s-%d-%d", + participantIdentifier, + maxSigners, + minSigners, + )), + }, + }, nil +} + +func (dnfdkg *deterministicNativeFROSTDKGEngine) Part2( + secretPackage *NativeFROSTDKGRound1SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, +) (*NativeFROSTDKGPart2Result, error) { + packages := make([]*NativeFROSTDKGRound2Package, 0, len(round1Packages)) + for _, round1Package := range round1Packages { + packages = append(packages, &NativeFROSTDKGRound2Package{ + Identifier: round1Package.Identifier, + Data: append( + []byte("round2-package-for-"+round1Package.Identifier+":"), + secretPackage.Data..., + ), + }) + } + + return &NativeFROSTDKGPart2Result{ + SecretPackage: &NativeFROSTDKGRound2SecretPackage{ + Data: []byte("round2-secret"), + }, + Packages: packages, + }, nil +} + +func (dnfdkg *deterministicNativeFROSTDKGEngine) Part3( + secretPackage *NativeFROSTDKGRound2SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + round2Packages []*NativeFROSTDKGRound2Package, +) (*NativeFROSTDKGResult, error) { + if len(round1Packages) != len(round2Packages) { + return nil, fmt.Errorf("unexpected package counts") + } + + return &NativeFROSTDKGResult{ + KeyPackage: &NativeFROSTKeyPackage{ + Identifier: round2Packages[0].Identifier, + Data: append([]byte{}, secretPackage.Data...), + }, + PublicKeyPackage: &NativeFROSTPublicKeyPackage{ + VerifyingShares: map[string]string{ + round1Packages[0].Identifier: "share", + }, + VerifyingKey: "1111111111111111111111111111111111111111111111111111111111111111", + }, + }, nil +} + +func (mnfdkg *mockNativeFROSTDKGEngine) Part2( + secretPackage *NativeFROSTDKGRound1SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, +) (*NativeFROSTDKGPart2Result, error) { + return nil, nil +} + +func (mnfdkg *mockNativeFROSTDKGEngine) Part3( + secretPackage *NativeFROSTDKGRound2SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + round2Packages []*NativeFROSTDKGRound2Package, +) (*NativeFROSTDKGResult, error) { + return nil, nil +} + +type mockUniFFINativeFROSTDKGBridge struct { + part1Called bool + part2Called bool + part3Called bool +} + +func (munfdkgb *mockUniFFINativeFROSTDKGBridge) Part1( + participantIdentifier string, + maxSigners uint16, + minSigners uint16, +) (*NativeFROSTDKGPart1Result, error) { + munfdkgb.part1Called = true + + return &NativeFROSTDKGPart1Result{ + SecretPackage: &NativeFROSTDKGRound1SecretPackage{Data: []byte{0x01}}, + Package: &NativeFROSTDKGRound1Package{ + Identifier: participantIdentifier, + Data: []byte{byte(maxSigners), byte(minSigners)}, + }, + }, nil +} + +func (munfdkgb *mockUniFFINativeFROSTDKGBridge) Part2( + secretPackage *NativeFROSTDKGRound1SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, +) (*NativeFROSTDKGPart2Result, error) { + munfdkgb.part2Called = true + + return &NativeFROSTDKGPart2Result{ + SecretPackage: &NativeFROSTDKGRound2SecretPackage{Data: []byte{0x02}}, + Packages: []*NativeFROSTDKGRound2Package{ + { + Identifier: round1Packages[0].Identifier, + Data: append([]byte{}, secretPackage.Data...), + }, + }, + }, nil +} + +func (munfdkgb *mockUniFFINativeFROSTDKGBridge) Part3( + secretPackage *NativeFROSTDKGRound2SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + round2Packages []*NativeFROSTDKGRound2Package, +) (*NativeFROSTDKGResult, error) { + munfdkgb.part3Called = true + + return &NativeFROSTDKGResult{ + KeyPackage: &NativeFROSTKeyPackage{ + Identifier: round2Packages[0].SenderIdentifier, + Data: append([]byte{}, secretPackage.Data...), + }, + PublicKeyPackage: &NativeFROSTPublicKeyPackage{ + VerifyingShares: map[string]string{ + round1Packages[0].Identifier: "share", + }, + VerifyingKey: "1111111111111111111111111111111111111111111111111111111111111111", + }, + }, nil +} + +func TestRegisterNativeFROSTDKGEngineRejectsNil(t *testing.T) { + UnregisterNativeFROSTDKGEngine() + t.Cleanup(UnregisterNativeFROSTDKGEngine) + + err := RegisterNativeFROSTDKGEngine(nil) + if err == nil { + t.Fatal("expected error") + } +} + +func TestRegisterNativeFROSTDKGEngine(t *testing.T) { + UnregisterNativeFROSTDKGEngine() + t.Cleanup(UnregisterNativeFROSTDKGEngine) + + engine := &mockNativeFROSTDKGEngine{} + if err := RegisterNativeFROSTDKGEngine(engine); err != nil { + t.Fatalf("unexpected register error: [%v]", err) + } + + if currentNativeFROSTDKGEngine() != engine { + t.Fatal("unexpected current native FROST DKG engine") + } +} + +func TestNewUniFFINativeFROSTDKGEngine_NilBridge(t *testing.T) { + _, err := newUniFFINativeFROSTDKGEngine(nil) + if err == nil { + t.Fatal("expected error") + } +} + +func TestUniFFINativeFROSTDKGEngine(t *testing.T) { + bridge := &mockUniFFINativeFROSTDKGBridge{} + engine, err := newUniFFINativeFROSTDKGEngine(bridge) + if err != nil { + t.Fatalf("unexpected constructor error: [%v]", err) + } + + part1, err := engine.Part1("participant-1", 3, 2) + if err != nil { + t.Fatalf("unexpected part1 error: [%v]", err) + } + + part2, err := engine.Part2( + part1.SecretPackage, + []*NativeFROSTDKGRound1Package{ + {Identifier: "participant-2", Data: []byte{0x22}}, + }, + ) + if err != nil { + t.Fatalf("unexpected part2 error: [%v]", err) + } + + _, err = engine.Part3( + part2.SecretPackage, + []*NativeFROSTDKGRound1Package{ + {Identifier: "participant-2", Data: []byte{0x22}}, + }, + []*NativeFROSTDKGRound2Package{ + { + Identifier: "participant-1", + SenderIdentifier: "participant-2", + Data: []byte{0x33}, + }, + }, + ) + if err != nil { + t.Fatalf("unexpected part3 error: [%v]", err) + } + + if !bridge.part1Called || !bridge.part2Called || !bridge.part3Called { + t.Fatal("expected all bridge parts to be called") + } +} + +func TestExecuteNativeFROSTDKG(t *testing.T) { + const channelName = "native-frost-dkg-test" + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + engine := &deterministicNativeFROSTDKGEngine{} + includedMembers := []group.MemberIndex{1, 2, 3} + + var wg sync.WaitGroup + errChan := make(chan error, len(includedMembers)) + for _, memberIndex := range includedMembers { + memberIndex := memberIndex + wg.Add(1) + + go func() { + defer wg.Done() + + provider := local.Connect() + channel, err := provider.BroadcastChannelFor(channelName) + if err != nil { + errChan <- err + return + } + RegisterNativeFROSTDKGUnmarshallers(channel) + + result, err := ExecuteNativeFROSTDKG( + ctx, + nil, + &NativeFROSTDKGRequest{ + MemberIndex: memberIndex, + GroupSize: 3, + Threshold: 2, + SessionID: "session-1", + IncludedMembersIndexes: includedMembers, + Channel: channel, + }, + engine, + ) + if err != nil { + errChan <- err + return + } + if result == nil { + errChan <- fmt.Errorf("nil DKG result") + } + }() + } + + wg.Wait() + close(errChan) + + for err := range errChan { + if err != nil { + t.Fatal(err) + } + } +} + +func TestNativeFROSTDKGResultSignerMaterial(t *testing.T) { + dkgResult := &NativeFROSTDKGResult{ + KeyPackage: &NativeFROSTKeyPackage{ + Identifier: "0000000000000000000000000000000000000000000000000000000000000001", + Data: []byte{0x01, 0x02, 0x03}, + }, + PublicKeyPackage: &NativeFROSTPublicKeyPackage{ + VerifyingShares: map[string]string{ + "0000000000000000000000000000000000000000000000000000000000000001": "share", + }, + VerifyingKey: "1111111111111111111111111111111111111111111111111111111111111111", + }, + } + + signerMaterial, err := dkgResult.SignerMaterial() + if err != nil { + t.Fatalf("unexpected signer material error: [%v]", err) + } + + if signerMaterial.Format != NativeSignerMaterialFormatFrostUniFFIV2 { + t.Fatalf( + "unexpected signer material format\nexpected: [%s]\nactual: [%s]", + NativeSignerMaterialFormatFrostUniFFIV2, + signerMaterial.Format, + ) + } + + extracted, err := ExtractDkgGroupPublicKeyFromMaterial(signerMaterial) + if err != nil { + t.Fatalf("unexpected DKG public-key extraction error: [%v]", err) + } + + expected := "1111111111111111111111111111111111111111111111111111111111111111" + if actual := stringHex(extracted); actual != expected { + t.Fatalf( + "unexpected extracted DKG output key\nexpected: [%s]\nactual: [%s]", + expected, + actual, + ) + } +} + +func stringHex(data []byte) string { + const hexChars = "0123456789abcdef" + result := make([]byte, len(data)*2) + for i, b := range data { + result[i*2] = hexChars[b>>4] + result[i*2+1] = hexChars[b&0x0f] + } + return string(result) +} diff --git a/pkg/frost/signing/native_frost_dkg_engine_uniffi_frost_native.go b/pkg/frost/signing/native_frost_dkg_engine_uniffi_frost_native.go new file mode 100644 index 0000000000..4db0e87ee7 --- /dev/null +++ b/pkg/frost/signing/native_frost_dkg_engine_uniffi_frost_native.go @@ -0,0 +1,157 @@ +//go:build frost_native + +package signing + +import "fmt" + +type uniFFINativeFROSTDKGBridge interface { + Part1( + participantIdentifier string, + maxSigners uint16, + minSigners uint16, + ) (*NativeFROSTDKGPart1Result, error) + Part2( + secretPackage *NativeFROSTDKGRound1SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + ) (*NativeFROSTDKGPart2Result, error) + Part3( + secretPackage *NativeFROSTDKGRound2SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + round2Packages []*NativeFROSTDKGRound2Package, + ) (*NativeFROSTDKGResult, error) +} + +type uniFFINativeFROSTDKGEngine struct { + bridge uniFFINativeFROSTDKGBridge +} + +func newUniFFINativeFROSTDKGEngine( + bridge uniFFINativeFROSTDKGBridge, +) (NativeFROSTDKGEngine, error) { + if bridge == nil { + return nil, fmt.Errorf("uniffi native FROST DKG bridge is nil") + } + + return &uniFFINativeFROSTDKGEngine{ + bridge: bridge, + }, nil +} + +func (unfdkg *uniFFINativeFROSTDKGEngine) Part1( + participantIdentifier string, + maxSigners uint16, + minSigners uint16, +) (*NativeFROSTDKGPart1Result, error) { + if participantIdentifier == "" { + return nil, fmt.Errorf("participant identifier is empty") + } + if maxSigners == 0 { + return nil, fmt.Errorf("max signers is zero") + } + if minSigners == 0 { + return nil, fmt.Errorf("min signers is zero") + } + if minSigners > maxSigners { + return nil, fmt.Errorf("min signers exceeds max signers") + } + + result, err := unfdkg.bridge.Part1( + participantIdentifier, + maxSigners, + minSigners, + ) + if err != nil { + return nil, err + } + + if err := validateNativeFROSTDKGPart1Result(result); err != nil { + return nil, err + } + + return result, nil +} + +func (unfdkg *uniFFINativeFROSTDKGEngine) Part2( + secretPackage *NativeFROSTDKGRound1SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, +) (*NativeFROSTDKGPart2Result, error) { + if secretPackage == nil { + return nil, fmt.Errorf("round-one secret package is nil") + } + if len(secretPackage.Data) == 0 { + return nil, fmt.Errorf("round-one secret package data is empty") + } + if len(round1Packages) == 0 { + return nil, fmt.Errorf("round-one packages are empty") + } + for i, pkg := range round1Packages { + if pkg == nil { + return nil, fmt.Errorf("round-one package [%d] is nil", i) + } + if pkg.Identifier == "" { + return nil, fmt.Errorf("round-one package [%d] identifier is empty", i) + } + if len(pkg.Data) == 0 { + return nil, fmt.Errorf("round-one package [%d] data is empty", i) + } + } + + result, err := unfdkg.bridge.Part2(secretPackage, round1Packages) + if err != nil { + return nil, err + } + + if err := validateNativeFROSTDKGPart2Result(result); err != nil { + return nil, err + } + + return result, nil +} + +func (unfdkg *uniFFINativeFROSTDKGEngine) Part3( + secretPackage *NativeFROSTDKGRound2SecretPackage, + round1Packages []*NativeFROSTDKGRound1Package, + round2Packages []*NativeFROSTDKGRound2Package, +) (*NativeFROSTDKGResult, error) { + if secretPackage == nil { + return nil, fmt.Errorf("round-two secret package is nil") + } + if len(secretPackage.Data) == 0 { + return nil, fmt.Errorf("round-two secret package data is empty") + } + if len(round1Packages) == 0 { + return nil, fmt.Errorf("round-one packages are empty") + } + if len(round2Packages) == 0 { + return nil, fmt.Errorf("round-two packages are empty") + } + for i, pkg := range round2Packages { + if pkg == nil { + return nil, fmt.Errorf("round-two package [%d] is nil", i) + } + if pkg.Identifier == "" { + return nil, fmt.Errorf("round-two package [%d] identifier is empty", i) + } + if pkg.SenderIdentifier == "" { + return nil, fmt.Errorf("round-two package [%d] sender identifier is empty", i) + } + if len(pkg.Data) == 0 { + return nil, fmt.Errorf("round-two package [%d] data is empty", i) + } + } + + result, err := unfdkg.bridge.Part3( + secretPackage, + round1Packages, + round2Packages, + ) + if err != nil { + return nil, err + } + + if err := validateNativeFROSTDKGResult(result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/pkg/frost/signing/native_frost_dkg_protocol_frost_native.go b/pkg/frost/signing/native_frost_dkg_protocol_frost_native.go new file mode 100644 index 0000000000..57270197ee --- /dev/null +++ b/pkg/frost/signing/native_frost_dkg_protocol_frost_native.go @@ -0,0 +1,689 @@ +//go:build frost_native + +package signing + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "sort" + "strconv" + + "github.com/ipfs/go-log/v2" + "github.com/keep-network/keep-core/pkg/net" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +const nativeFROSTDKGMessageTypePrefix = "frost_dkg/native_frost/" + +// NativeFROSTDKGRequest contains the local participant context needed to run +// the native FROST DKG protocol over a keep-core broadcast channel. +type NativeFROSTDKGRequest struct { + MemberIndex group.MemberIndex + GroupSize int + Threshold int + SessionID string + IncludedMembersIndexes []group.MemberIndex + Channel net.BroadcastChannel + MembershipValidator *group.MembershipValidator +} + +type nativeFROSTDKGRoundOnePackageMessage struct { + SenderIDValue uint32 `json:"senderID"` + SessionIDValue string `json:"sessionID"` + ParticipantIdentifier string `json:"participantIdentifier"` + PackageData []byte `json:"packageData"` +} + +func (nfdkgropm *nativeFROSTDKGRoundOnePackageMessage) SenderID() group.MemberIndex { + return group.MemberIndex(nfdkgropm.SenderIDValue) +} + +func (nfdkgropm *nativeFROSTDKGRoundOnePackageMessage) SessionID() string { + return nfdkgropm.SessionIDValue +} + +func (nfdkgropm *nativeFROSTDKGRoundOnePackageMessage) Type() string { + return nativeFROSTDKGMessageTypePrefix + "round_one_package" +} + +func (nfdkgropm *nativeFROSTDKGRoundOnePackageMessage) Marshal() ([]byte, error) { + return json.Marshal(nfdkgropm) +} + +func (nfdkgropm *nativeFROSTDKGRoundOnePackageMessage) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, nfdkgropm); err != nil { + return err + } + + if nfdkgropm.SenderID() == 0 { + return fmt.Errorf("sender ID is zero") + } + if nfdkgropm.SessionID() == "" { + return fmt.Errorf("session ID is empty") + } + if nfdkgropm.ParticipantIdentifier == "" { + return fmt.Errorf("participant identifier is empty") + } + if len(nfdkgropm.PackageData) == 0 { + return fmt.Errorf("round-one package data is empty") + } + + return nil +} + +type nativeFROSTDKGRoundTwoPackageMessage struct { + SenderIDValue uint32 `json:"senderID"` + ReceiverIDValue uint32 `json:"receiverID"` + SessionIDValue string `json:"sessionID"` + SenderParticipantIdentifier string `json:"senderParticipantIdentifier"` + PackageParticipantIdentifier string `json:"packageParticipantIdentifier"` + PackageData []byte `json:"packageData"` +} + +func (nfdkgtrpm *nativeFROSTDKGRoundTwoPackageMessage) SenderID() group.MemberIndex { + return group.MemberIndex(nfdkgtrpm.SenderIDValue) +} + +func (nfdkgtrpm *nativeFROSTDKGRoundTwoPackageMessage) ReceiverID() group.MemberIndex { + return group.MemberIndex(nfdkgtrpm.ReceiverIDValue) +} + +func (nfdkgtrpm *nativeFROSTDKGRoundTwoPackageMessage) SessionID() string { + return nfdkgtrpm.SessionIDValue +} + +func (nfdkgtrpm *nativeFROSTDKGRoundTwoPackageMessage) Type() string { + return nativeFROSTDKGMessageTypePrefix + "round_two_package" +} + +func (nfdkgtrpm *nativeFROSTDKGRoundTwoPackageMessage) Marshal() ([]byte, error) { + return json.Marshal(nfdkgtrpm) +} + +func (nfdkgtrpm *nativeFROSTDKGRoundTwoPackageMessage) Unmarshal(data []byte) error { + if err := json.Unmarshal(data, nfdkgtrpm); err != nil { + return err + } + + if nfdkgtrpm.SenderID() == 0 { + return fmt.Errorf("sender ID is zero") + } + if nfdkgtrpm.ReceiverID() == 0 { + return fmt.Errorf("receiver ID is zero") + } + if nfdkgtrpm.SessionID() == "" { + return fmt.Errorf("session ID is empty") + } + if nfdkgtrpm.SenderParticipantIdentifier == "" { + return fmt.Errorf("sender participant identifier is empty") + } + if nfdkgtrpm.PackageParticipantIdentifier == "" { + return fmt.Errorf("package participant identifier is empty") + } + if len(nfdkgtrpm.PackageData) == 0 { + return fmt.Errorf("round-two package data is empty") + } + + return nil +} + +// RegisterNativeFROSTDKGUnmarshallers registers native FROST DKG protocol +// messages on the given broadcast channel. +func RegisterNativeFROSTDKGUnmarshallers(channel net.BroadcastChannel) { + channel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &nativeFROSTDKGRoundOnePackageMessage{} + }) + channel.SetUnmarshaler(func() net.TaggedUnmarshaler { + return &nativeFROSTDKGRoundTwoPackageMessage{} + }) +} + +// NativeFROSTParticipantIdentifierForMemberIndex returns the participant +// identifier string expected by the UniFFI FROST SDK for a keep-core 1-based +// group member index. +func NativeFROSTParticipantIdentifierForMemberIndex( + memberIndex group.MemberIndex, +) (string, error) { + if memberIndex == 0 { + return "", fmt.Errorf("member index is zero") + } + if memberIndex > group.MaxMemberIndex { + return "", fmt.Errorf("member index [%v] exceeds maximum", memberIndex) + } + + // frost-core serializes Identifier::try_from(n) as a 32-byte little-endian + // scalar hex string wrapped as JSON. DKG group sizes are bounded to uint8 + // indexes in keep-core, so setting the first byte is sufficient. + var identifier [32]byte + identifier[0] = byte(memberIndex) + + return strconv.Quote(hex.EncodeToString(identifier[:])), nil +} + +// ExecuteNativeFROSTDKG executes the three native FROST DKG rounds. The caller +// is responsible for scoping ctx to the on-chain submission timeout. +func ExecuteNativeFROSTDKG( + ctx context.Context, + logger log.StandardLogger, + request *NativeFROSTDKGRequest, + engine NativeFROSTDKGEngine, +) (*NativeFROSTDKGResult, error) { + if engine == nil { + return nil, fmt.Errorf( + "%w: native FROST DKG engine is unavailable", + ErrNativeCryptographyUnavailable, + ) + } + + includedMembersSet, includedMembersIndexes, err := + includedMembersFromDKGRequest(request) + if err != nil { + return nil, err + } + + identifiersByMemberIndex, memberIndexesByIdentifier, err := + nativeFROSTDKGParticipantIdentifiers(includedMembersIndexes) + if err != nil { + return nil, err + } + + ownIdentifier := identifiersByMemberIndex[request.MemberIndex] + part1, err := engine.Part1( + ownIdentifier, + uint16(len(includedMembersIndexes)), + uint16(request.Threshold), + ) + if err != nil { + return nil, fmt.Errorf("native FROST DKG part one failed: [%w]", err) + } + if err := validateNativeFROSTDKGPart1Result(part1); err != nil { + return nil, err + } + + roundOneMessage := &nativeFROSTDKGRoundOnePackageMessage{ + SenderIDValue: uint32(request.MemberIndex), + SessionIDValue: request.SessionID, + ParticipantIdentifier: part1.Package.Identifier, + PackageData: append([]byte{}, part1.Package.Data...), + } + if err := request.Channel.Send( + ctx, + roundOneMessage, + net.BackoffRetransmissionStrategy, + ); err != nil { + return nil, fmt.Errorf("cannot send native FROST DKG round-one package: [%w]", err) + } + + roundOneMessages, err := collectNativeFROSTDKGRoundOnePackageMessages( + ctx, + request, + includedMembersSet, + includedMembersIndexes, + ) + if err != nil { + return nil, err + } + + roundOnePackages := make( + []*NativeFROSTDKGRound1Package, + 0, + len(roundOneMessages), + ) + for _, memberIndex := range includedMembersIndexes { + if memberIndex == request.MemberIndex { + continue + } + + message := roundOneMessages[memberIndex] + roundOnePackages = append(roundOnePackages, &NativeFROSTDKGRound1Package{ + Identifier: message.ParticipantIdentifier, + Data: append([]byte{}, message.PackageData...), + }) + } + + part2, err := engine.Part2(part1.SecretPackage, roundOnePackages) + if err != nil { + return nil, fmt.Errorf("native FROST DKG part two failed: [%w]", err) + } + if err := validateNativeFROSTDKGPart2Result(part2); err != nil { + return nil, err + } + + for _, pkg := range part2.Packages { + receiverID, ok := memberIndexesByIdentifier[pkg.Identifier] + if !ok { + return nil, fmt.Errorf( + "native FROST DKG part two produced package for unknown identifier [%s]", + pkg.Identifier, + ) + } + if receiverID == request.MemberIndex { + return nil, fmt.Errorf( + "native FROST DKG part two produced package for self", + ) + } + + roundTwoMessage := &nativeFROSTDKGRoundTwoPackageMessage{ + SenderIDValue: uint32(request.MemberIndex), + ReceiverIDValue: uint32(receiverID), + SessionIDValue: request.SessionID, + SenderParticipantIdentifier: ownIdentifier, + PackageParticipantIdentifier: pkg.Identifier, + PackageData: append([]byte{}, pkg.Data...), + } + if err := request.Channel.Send( + ctx, + roundTwoMessage, + net.BackoffRetransmissionStrategy, + ); err != nil { + return nil, fmt.Errorf("cannot send native FROST DKG round-two package: [%w]", err) + } + } + + roundTwoMessages, err := collectNativeFROSTDKGRoundTwoPackageMessages( + ctx, + request, + includedMembersSet, + includedMembersIndexes, + ) + if err != nil { + return nil, err + } + + roundTwoPackages := make( + []*NativeFROSTDKGRound2Package, + 0, + len(roundTwoMessages), + ) + for _, memberIndex := range includedMembersIndexes { + if memberIndex == request.MemberIndex { + continue + } + + message := roundTwoMessages[memberIndex] + roundTwoPackages = append(roundTwoPackages, &NativeFROSTDKGRound2Package{ + Identifier: message.PackageParticipantIdentifier, + SenderIdentifier: message.SenderParticipantIdentifier, + Data: append([]byte{}, message.PackageData...), + }) + } + + result, err := engine.Part3(part2.SecretPackage, roundOnePackages, roundTwoPackages) + if err != nil { + return nil, fmt.Errorf("native FROST DKG part three failed: [%w]", err) + } + if err := validateNativeFROSTDKGResult(result); err != nil { + return nil, err + } + + if logger != nil { + logger.Debugf( + "[member:%v] native FROST DKG completed with [%v] participants", + request.MemberIndex, + len(includedMembersIndexes), + ) + } + + return result, nil +} + +func includedMembersFromDKGRequest( + request *NativeFROSTDKGRequest, +) (map[group.MemberIndex]struct{}, []group.MemberIndex, error) { + if request == nil { + return nil, nil, fmt.Errorf("request is nil") + } + if request.MemberIndex == 0 { + return nil, nil, fmt.Errorf("member index is zero") + } + if request.GroupSize <= 0 { + return nil, nil, fmt.Errorf("group size must be positive") + } + if request.Threshold <= 0 { + return nil, nil, fmt.Errorf("threshold must be positive") + } + if request.SessionID == "" { + return nil, nil, fmt.Errorf("session ID is empty") + } + if request.Channel == nil { + return nil, nil, fmt.Errorf("broadcast channel is nil") + } + if request.GroupSize > int(group.MaxMemberIndex) { + return nil, nil, fmt.Errorf("group size [%d] exceeds maximum", request.GroupSize) + } + + includedMembersSet := make(map[group.MemberIndex]struct{}) + if len(request.IncludedMembersIndexes) > 0 { + for _, memberIndex := range request.IncludedMembersIndexes { + if memberIndex == 0 || int(memberIndex) > request.GroupSize { + return nil, nil, fmt.Errorf( + "included member index [%v] out of range [1, %d]", + memberIndex, + request.GroupSize, + ) + } + + includedMembersSet[memberIndex] = struct{}{} + } + } else { + for i := 1; i <= request.GroupSize; i++ { + includedMembersSet[group.MemberIndex(i)] = struct{}{} + } + } + + if _, ok := includedMembersSet[request.MemberIndex]; !ok { + return nil, nil, fmt.Errorf( + "member [%v] not included in native FROST DKG attempt", + request.MemberIndex, + ) + } + if len(includedMembersSet) < request.Threshold { + return nil, nil, fmt.Errorf( + "included members count [%d] is below threshold [%d]", + len(includedMembersSet), + request.Threshold, + ) + } + if len(includedMembersSet) > int(^uint16(0)) || + request.Threshold > int(^uint16(0)) { + return nil, nil, fmt.Errorf("native FROST DKG parameters exceed uint16") + } + + 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 nativeFROSTDKGParticipantIdentifiers( + memberIndexes []group.MemberIndex, +) ( + map[group.MemberIndex]string, + map[string]group.MemberIndex, + error, +) { + identifiersByMemberIndex := make(map[group.MemberIndex]string, len(memberIndexes)) + memberIndexesByIdentifier := make(map[string]group.MemberIndex, len(memberIndexes)) + + for _, memberIndex := range memberIndexes { + identifier, err := NativeFROSTParticipantIdentifierForMemberIndex(memberIndex) + if err != nil { + return nil, nil, err + } + + identifiersByMemberIndex[memberIndex] = identifier + memberIndexesByIdentifier[identifier] = memberIndex + } + + return identifiersByMemberIndex, memberIndexesByIdentifier, nil +} + +func collectNativeFROSTDKGRoundOnePackageMessages( + ctx context.Context, + request *NativeFROSTDKGRequest, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, +) (map[group.MemberIndex]*nativeFROSTDKGRoundOnePackageMessage, error) { + expectedMessagesCount := len(includedMembersIndexes) - 1 + if expectedMessagesCount <= 0 { + return map[group.MemberIndex]*nativeFROSTDKGRoundOnePackageMessage{}, nil + } + + recvCtx, cancelRecvCtx := context.WithCancel(ctx) + defer cancelRecvCtx() + + messageChan := make(chan *nativeFROSTDKGRoundOnePackageMessage, expectedMessagesCount*4+1) + request.Channel.Recv(recvCtx, func(message net.Message) { + payload, ok := message.Payload().(*nativeFROSTDKGRoundOnePackageMessage) + if !ok { + return + } + + if !shouldAcceptNativeFROSTDKGMessage( + request, + includedMembersSet, + payload.SenderID(), + payload.SessionID(), + message.SenderPublicKey(), + ) { + return + } + + select { + case messageChan <- payload: + default: + protocolLogger.Warnf( + "dropping native FROST DKG round-one package from sender [%d]; collector buffer full", + payload.SenderID(), + ) + } + }) + + receivedMessages := make(map[group.MemberIndex]*nativeFROSTDKGRoundOnePackageMessage) + for len(receivedMessages) < expectedMessagesCount { + select { + case <-ctx.Done(): + return nil, fmt.Errorf( + "native FROST DKG round-one collection interrupted: [%w]", + ctx.Err(), + ) + case message := <-messageChan: + senderID := message.SenderID() + if existing, ok := receivedMessages[senderID]; ok { + if !nativeFROSTDKGRoundOnePackageMessagesEqual(existing, message) { + protocolLogger.Warnf( + "dropping conflicting native FROST DKG round-one package from sender [%d]", + senderID, + ) + } + continue + } + receivedMessages[senderID] = message + } + } + + return receivedMessages, nil +} + +func collectNativeFROSTDKGRoundTwoPackageMessages( + ctx context.Context, + request *NativeFROSTDKGRequest, + includedMembersSet map[group.MemberIndex]struct{}, + includedMembersIndexes []group.MemberIndex, +) (map[group.MemberIndex]*nativeFROSTDKGRoundTwoPackageMessage, error) { + expectedMessagesCount := len(includedMembersIndexes) - 1 + if expectedMessagesCount <= 0 { + return map[group.MemberIndex]*nativeFROSTDKGRoundTwoPackageMessage{}, nil + } + + recvCtx, cancelRecvCtx := context.WithCancel(ctx) + defer cancelRecvCtx() + + messageChan := make(chan *nativeFROSTDKGRoundTwoPackageMessage, expectedMessagesCount*4+1) + request.Channel.Recv(recvCtx, func(message net.Message) { + payload, ok := message.Payload().(*nativeFROSTDKGRoundTwoPackageMessage) + if !ok { + return + } + + if payload.ReceiverID() != request.MemberIndex { + return + } + + if !shouldAcceptNativeFROSTDKGMessage( + request, + includedMembersSet, + payload.SenderID(), + payload.SessionID(), + message.SenderPublicKey(), + ) { + return + } + + select { + case messageChan <- payload: + default: + protocolLogger.Warnf( + "dropping native FROST DKG round-two package from sender [%d]; collector buffer full", + payload.SenderID(), + ) + } + }) + + receivedMessages := make(map[group.MemberIndex]*nativeFROSTDKGRoundTwoPackageMessage) + for len(receivedMessages) < expectedMessagesCount { + select { + case <-ctx.Done(): + return nil, fmt.Errorf( + "native FROST DKG round-two collection interrupted: [%w]", + ctx.Err(), + ) + case message := <-messageChan: + senderID := message.SenderID() + if existing, ok := receivedMessages[senderID]; ok { + if !nativeFROSTDKGRoundTwoPackageMessagesEqual(existing, message) { + protocolLogger.Warnf( + "dropping conflicting native FROST DKG round-two package from sender [%d]", + senderID, + ) + } + continue + } + receivedMessages[senderID] = message + } + } + + return receivedMessages, nil +} + +func shouldAcceptNativeFROSTDKGMessage( + request *NativeFROSTDKGRequest, + 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 nativeFROSTDKGRoundOnePackageMessagesEqual( + left, right *nativeFROSTDKGRoundOnePackageMessage, +) bool { + if left == nil || right == nil { + return left == right + } + + return left.SenderIDValue == right.SenderIDValue && + left.SessionIDValue == right.SessionIDValue && + left.ParticipantIdentifier == right.ParticipantIdentifier && + bytes.Equal(left.PackageData, right.PackageData) +} + +func nativeFROSTDKGRoundTwoPackageMessagesEqual( + left, right *nativeFROSTDKGRoundTwoPackageMessage, +) bool { + if left == nil || right == nil { + return left == right + } + + return left.SenderIDValue == right.SenderIDValue && + left.ReceiverIDValue == right.ReceiverIDValue && + left.SessionIDValue == right.SessionIDValue && + left.SenderParticipantIdentifier == right.SenderParticipantIdentifier && + left.PackageParticipantIdentifier == right.PackageParticipantIdentifier && + bytes.Equal(left.PackageData, right.PackageData) +} + +func validateNativeFROSTDKGPart1Result(result *NativeFROSTDKGPart1Result) error { + if result == nil { + return fmt.Errorf("native FROST DKG part one result is nil") + } + if result.SecretPackage == nil { + return fmt.Errorf("native FROST DKG part one secret package is nil") + } + if len(result.SecretPackage.Data) == 0 { + return fmt.Errorf("native FROST DKG part one secret package data is empty") + } + if result.Package == nil { + return fmt.Errorf("native FROST DKG part one package is nil") + } + if result.Package.Identifier == "" { + return fmt.Errorf("native FROST DKG part one package identifier is empty") + } + if len(result.Package.Data) == 0 { + return fmt.Errorf("native FROST DKG part one package data is empty") + } + + return nil +} + +func validateNativeFROSTDKGPart2Result(result *NativeFROSTDKGPart2Result) error { + if result == nil { + return fmt.Errorf("native FROST DKG part two result is nil") + } + if result.SecretPackage == nil { + return fmt.Errorf("native FROST DKG part two secret package is nil") + } + if len(result.SecretPackage.Data) == 0 { + return fmt.Errorf("native FROST DKG part two secret package data is empty") + } + if len(result.Packages) == 0 { + return fmt.Errorf("native FROST DKG part two packages are empty") + } + for i, pkg := range result.Packages { + if pkg == nil { + return fmt.Errorf("native FROST DKG part two package [%d] is nil", i) + } + if pkg.Identifier == "" { + return fmt.Errorf( + "native FROST DKG part two package [%d] identifier is empty", + i, + ) + } + if len(pkg.Data) == 0 { + return fmt.Errorf( + "native FROST DKG part two package [%d] data is empty", + i, + ) + } + } + + return nil +} + +func validateNativeFROSTDKGResult(result *NativeFROSTDKGResult) error { + if result == nil { + return fmt.Errorf("native FROST DKG result is nil") + } + + _, err := result.SignerMaterial() + return err +} diff --git a/pkg/tbtc/frost_dkg_chain.go b/pkg/tbtc/frost_dkg_chain.go new file mode 100644 index 0000000000..1e3263f357 --- /dev/null +++ b/pkg/tbtc/frost_dkg_chain.go @@ -0,0 +1,86 @@ +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 *DKGStartedEventFilter, + ) ([]*FrostDKGStartedEvent, error) + + OnFrostDKGResultSubmitted( + func(event *FrostDKGResultSubmittedEvent), + ) subscription.EventSubscription + PastFrostDKGResultSubmittedEvents( + filter *DKGStartedEventFilter, + ) ([]*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 +} + +// FrostDKGResultSubmittedEvent represents a FROST DKG result submission. +type FrostDKGResultSubmittedEvent struct { + Seed *big.Int + ResultHash DKGChainResultHash + Result *registry.Result + BlockNumber uint64 +} + +// 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..48030b89f4 --- /dev/null +++ b/pkg/tbtc/frost_dkg_coordinator.go @@ -0,0 +1,395 @@ +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( + &DKGStartedEventFilter{ + 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: + events, err := frostChain.PastFrostDKGStartedEvents( + &DKGStartedEventFilter{StartBlock: 0}, + ) + 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: + events, err := frostChain.PastFrostDKGResultSubmittedEvents( + &DKGStartedEventFilter{StartBlock: 0}, + ) + 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, + ) + if err := frostChain.ChallengeFrostDKGResult(event.Result); err != nil { + logger.Errorf( + "failed to challenge FROST DKG result [0x%x]: [%v]", + event.ResultHash, + err, + ) + } + 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 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 +} 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..a80167abb4 --- /dev/null +++ b/pkg/tbtc/frost_dkg_execution_frost_native.go @@ -0,0 +1,435 @@ +//go:build frost_native + +package tbtc + +import ( + "context" + "crypto/ecdsa" + "fmt" + + "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" +) + +func executeFrostDKGIfPossible( + ctx context.Context, + node *node, + frostChain FrostDKGChain, + event *FrostDKGStartedEvent, + memberIndexes []group.MemberIndex, + groupSelectionResult *GroupSelectionResult, +) { + engine := frostsigning.CurrentNativeFROSTDKGEngine() + if engine == nil { + logger.Infof( + "FROST DKG with seed [0x%x] selected this operator as member "+ + "indexes [%v], but no native FROST DKG 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 + } + + frostsigning.RegisterNativeFROSTDKGUnmarshallers(channel) + 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 + } + + 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 + } + + nativeResult, err := frostsigning.ExecuteNativeFROSTDKG( + dkgCtx, + dkgLogger, + &frostsigning.NativeFROSTDKGRequest{ + MemberIndex: memberIndex, + GroupSize: len(groupSelectionResult.OperatorsIDs), + Threshold: node.groupParameters.GroupQuorum, + SessionID: sessionID, + IncludedMembersIndexes: activeMemberIndexes, + Channel: channel, + MembershipValidator: membershipValidator, + }, + engine, + ) + if err != nil { + dkgLogger.Errorf("native FROST DKG execution failed: [%v]", err) + return + } + + if err := registerFrostSigner( + node, + nativeResult, + memberIndex, + activeMemberIndexes, + groupSelectionResult, + ); err != nil { + dkgLogger.Errorf("failed to register FROST signer: [%v]", err) + return + } + + outputKey, err := outputKeyFromNativeDKGResult(nativeResult) + if err != nil { + dkgLogger.Errorf("failed to extract FROST DKG output key: [%v]", err) + return + } + + unsignedResult, err := registry.AssembleResult( + uint64(memberIndex), + 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 err := submitFrostDKGResultWithDelay( + dkgCtx, + node, + frostChain, + memberIndex, + activeMemberIndexes, + signedResult, + ); err != nil { + dkgLogger.Errorf("failed to submit FROST DKG result: [%v]", err) + return + } + }() + } +} + +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 registerFrostSigner( + node *node, + nativeResult *frostsigning.NativeFROSTDKGResult, + memberIndex group.MemberIndex, + activeMemberIndexes []group.MemberIndex, + groupSelectionResult *GroupSelectionResult, +) error { + signerMaterial, err := nativeResult.SignerMaterial() + if err != nil { + return err + } + + outputKey, err := outputKeyFromNativeDKGResult(nativeResult) + if err != nil { + return err + } + + 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 := 0 + for i, activeMemberIndex := range activeMemberIndexes { + if activeMemberIndex == memberIndex { + rank = i + break + } + } + + blockCounter, err := node.chain.BlockCounter() + if err != nil { + return err + } + + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return err + } + + submissionBlock := currentBlock + uint64(rank)*dkgResultSubmissionDelayStepBlocks + 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 outputKeyFromNativeDKGResult( + nativeResult *frostsigning.NativeFROSTDKGResult, +) (frost.OutputKey, error) { + signerMaterial, err := nativeResult.SignerMaterial() + if err != nil { + return frost.OutputKey{}, err + } + + outputKeyBytes, err := frostsigning.ExtractDkgGroupPublicKeyFromMaterial( + signerMaterial, + ) + if err != nil { + return frost.OutputKey{}, err + } + if len(outputKeyBytes) != frost.OutputKeySize { + return frost.OutputKey{}, fmt.Errorf( + "unexpected FROST DKG output key length [%d]", + len(outputKeyBytes), + ) + } + + var outputKey frost.OutputKey + copy(outputKey[:], outputKeyBytes) + + return outputKey, nil +} + +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 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_result_signing.go b/pkg/tbtc/frost_dkg_result_signing.go new file mode 100644 index 0000000000..9033c6bf22 --- /dev/null +++ b/pkg/tbtc/frost_dkg_result_signing.go @@ -0,0 +1,271 @@ +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 := node.groupParameters.GroupQuorum + if expectedSignaturesCount > len(includedMembersIndexes) { + return nil, fmt.Errorf( + "FROST DKG included members count [%d] is below quorum [%d]", + len(includedMembersIndexes), + expectedSignaturesCount, + ) + } + + recvCtx, cancelRecvCtx := context.WithCancel(ctx) + defer cancelRecvCtx() + + 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/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/tbtc.go b/pkg/tbtc/tbtc.go index a2b5620ed4..35872bf415 100644 --- a/pkg/tbtc/tbtc.go +++ b/pkg/tbtc/tbtc.go @@ -118,6 +118,10 @@ 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( 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..a8009869a7 --- /dev/null +++ b/pkg/tbtc/wallet_id_from_signer_frost_native.go @@ -0,0 +1,79 @@ +//go:build frost_native + +package tbtc + +import ( + "crypto/ecdsa" + "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 + } + + if material.Format != frostsigning.NativeSignerMaterialFormatFrostUniFFIV2 { + return [32]byte{}, false, nil + } + + xOnlyOutputKey, err := frostsigning.ExtractDkgGroupPublicKeyFromMaterial( + 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..92c19c6bc7 --- /dev/null +++ b/pkg/tbtc/wallet_id_from_signer_frost_native_test.go @@ -0,0 +1,63 @@ +//go:build frost_native + +package tbtc + +import ( + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "testing" + + frostsigning "github.com/keep-network/keep-core/pkg/frost/signing" +) + +func TestCalculateWalletIDForSigner_FrostUniFFIV2UsesXOnlyOutputKey(t *testing.T) { + const xOnlyOutputKey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + 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: xOnlyOutputKey, + }, + }) + if err != nil { + t.Fatalf("unexpected payload marshal error: [%v]", err) + } + + signer := createMockSigner(t) + signer.signerMaterial = &frostsigning.NativeSignerMaterial{ + Format: frostsigning.NativeSignerMaterialFormatFrostUniFFIV2, + Payload: payload, + } + + walletID, err := calculateWalletIDForSigner( + signer, + func(_ *ecdsa.PublicKey) ([32]byte, error) { + return [32]byte{0xff}, nil + }, + ) + if err != nil { + t.Fatalf("unexpected wallet ID calculation error: [%v]", err) + } + + 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, + ) + } +} From 4bb1e5d95aed01066ff2cb3411c3ef2df898631c Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 25 May 2026 07:13:46 -0500 Subject: [PATCH 2/6] fix(frost): include generated bindings in docker context --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 9c48e7b076..6c0434c094 100644 --- a/.dockerignore +++ b/.dockerignore @@ -31,6 +31,8 @@ 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 # Legacy V1 contracts bindings. # We won't generate new bindings in the docker build process, but use the existing ones. From 71ce18a01d326edffbfdc1c7d26ed43b85cba789 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 25 May 2026 08:54:58 -0500 Subject: [PATCH 3/6] fix(frost): address DKG coordinator review feedback --- pkg/chain/ethereum/frost_dkg.go | 7 +- pkg/frost/registry/dkg_result_test.go | 72 ++++++++++++++++++- pkg/frost/registry/testdata/README.md | 16 +++++ .../registry/testdata/v4_digest_fixture.json | 15 +++- pkg/tbtc/frost_dkg_chain.go | 21 +++++- pkg/tbtc/frost_dkg_coordinator.go | 45 +++++++++++- pkg/tbtc/frost_dkg_execution_frost_native.go | 51 +++++++++++-- .../frost_dkg_execution_frost_native_test.go | 43 +++++++++++ pkg/tbtc/frost_dkg_parameters.go | 26 +++++++ pkg/tbtc/frost_dkg_parameters_test.go | 69 ++++++++++++++++++ pkg/tbtc/frost_dkg_result_signing.go | 14 +++- 11 files changed, 361 insertions(+), 18 deletions(-) create mode 100644 pkg/frost/registry/testdata/README.md create mode 100644 pkg/tbtc/frost_dkg_execution_frost_native_test.go create mode 100644 pkg/tbtc/frost_dkg_parameters.go create mode 100644 pkg/tbtc/frost_dkg_parameters_test.go diff --git a/pkg/chain/ethereum/frost_dkg.go b/pkg/chain/ethereum/frost_dkg.go index 43b5a20849..b09a980e36 100644 --- a/pkg/chain/ethereum/frost_dkg.go +++ b/pkg/chain/ethereum/frost_dkg.go @@ -88,7 +88,7 @@ func (tc *TbtcChain) OnFrostDKGStarted( // PastFrostDKGStartedEvents fetches past FROST DKG started events. func (tc *TbtcChain) PastFrostDKGStartedEvents( - filter *tbtc.DKGStartedEventFilter, + filter *tbtc.FrostDKGStartedEventFilter, ) ([]*tbtc.FrostDKGStartedEvent, error) { if tc.frostWalletRegistry == nil { return nil, fmt.Errorf("FrostWalletRegistry is not configured") @@ -195,7 +195,7 @@ func (tc *TbtcChain) OnFrostDKGResultSubmitted( // PastFrostDKGResultSubmittedEvents fetches past FROST DKG submitted events. func (tc *TbtcChain) PastFrostDKGResultSubmittedEvents( - filter *tbtc.DKGStartedEventFilter, + filter *tbtc.FrostDKGResultSubmittedEventFilter, ) ([]*tbtc.FrostDKGResultSubmittedEvent, error) { if tc.frostWalletRegistry == nil { return nil, fmt.Errorf("FrostWalletRegistry is not configured") @@ -209,6 +209,9 @@ func (tc *TbtcChain) PastFrostDKGResultSubmittedEvents( if filter != nil { startBlock = filter.StartBlock endBlock = filter.EndBlock + for _, hash := range filter.ResultHash { + resultHash = append(resultHash, [32]byte(hash)) + } seed = filter.Seed } diff --git a/pkg/frost/registry/dkg_result_test.go b/pkg/frost/registry/dkg_result_test.go index 93d0e71fb0..b06d09b9ea 100644 --- a/pkg/frost/registry/dkg_result_test.go +++ b/pkg/frost/registry/dkg_result_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "math/big" "os" + "path/filepath" + "runtime" "strings" "testing" @@ -12,7 +14,14 @@ import ( "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"` @@ -24,6 +33,15 @@ type v4DigestFixture struct { 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) { @@ -163,6 +181,58 @@ func TestEthereumSignedMessageHash(t *testing.T) { } } +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}, @@ -187,7 +257,7 @@ func TestActiveMembersFromMisbehavedRejectsInvalidIndices(t *testing.T) { func loadV4DigestFixture(t *testing.T) *v4DigestFixture { t.Helper() - data, err := os.ReadFile("testdata/v4_digest_fixture.json") + data, err := os.ReadFile(v4DigestFixtureTestPath) if err != nil { t.Fatalf("cannot read fixture: [%v]", err) } 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 index 2bc7662088..18fcfcb926 100644 --- a/pkg/frost/registry/testdata/v4_digest_fixture.json +++ b/pkg/frost/registry/testdata/v4_digest_fixture.json @@ -1,5 +1,11 @@ { - "description": "FROST DKG resultDigest v4 fixture generated from the tBTC TypeScript reference flow.", + "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", @@ -10,5 +16,10 @@ "fullMembersHash": "0x048553822a9f886e64193ef9da3f71732a9708edb45cff48e68466e635f6dbf7", "activeMembersHash": "0x4c78efa0361537bf6929c6e824b152e9b9be9140da28cbd9e1e569e483c4a16f", "digest": "0x45c93e369c6e4b43cbeebf09c7c639526ea0b826d3e89d87c1cd137a08dfc1d9", - "ethereumSignedMessageHash": "0xd70747a38b3969e9d95700a9fc7eae13883f9ee960290d7aee8114623fb9d6c9" + "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/tbtc/frost_dkg_chain.go b/pkg/tbtc/frost_dkg_chain.go index 1e3263f357..94a168fb9f 100644 --- a/pkg/tbtc/frost_dkg_chain.go +++ b/pkg/tbtc/frost_dkg_chain.go @@ -22,14 +22,14 @@ type FrostDKGChain interface { func(event *FrostDKGStartedEvent), ) subscription.EventSubscription PastFrostDKGStartedEvents( - filter *DKGStartedEventFilter, + filter *FrostDKGStartedEventFilter, ) ([]*FrostDKGStartedEvent, error) OnFrostDKGResultSubmitted( func(event *FrostDKGResultSubmittedEvent), ) subscription.EventSubscription PastFrostDKGResultSubmittedEvents( - filter *DKGStartedEventFilter, + filter *FrostDKGResultSubmittedEventFilter, ) ([]*FrostDKGResultSubmittedEvent, error) OnFrostDKGResultChallenged( func(event *FrostDKGResultChallengedEvent), @@ -62,6 +62,14 @@ type FrostDKGStartedEvent struct { 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 @@ -70,6 +78,15 @@ type FrostDKGResultSubmittedEvent struct { 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 diff --git a/pkg/tbtc/frost_dkg_coordinator.go b/pkg/tbtc/frost_dkg_coordinator.go index 48030b89f4..3690bb8576 100644 --- a/pkg/tbtc/frost_dkg_coordinator.go +++ b/pkg/tbtc/frost_dkg_coordinator.go @@ -7,6 +7,11 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/group" ) +// frostDKGRecoveryLookBackBlocks bounds cold-start eth_getLogs queries. +// FROST DKG recovery only needs recent AwaitingResult/Challenge events, and +// keeping the range at 10k blocks stays inside common RPC provider limits. +const frostDKGRecoveryLookBackBlocks = 10000 + func initializeFrostDKGCoordinator( ctx context.Context, node *node, @@ -107,7 +112,7 @@ func handleFrostDKGStarted( } pastEvents, err := frostChain.PastFrostDKGStartedEvents( - &DKGStartedEventFilter{ + &FrostDKGStartedEventFilter{ StartBlock: startBlock, }, ) @@ -164,8 +169,14 @@ func recoverFrostDKGCoordinatorState( switch state { case AwaitingResult: + startBlock, err := frostDKGRecoveryStartBlock(node) + if err != nil { + logger.Errorf("failed to resolve FROST DKG recovery start block: [%v]", err) + return + } + events, err := frostChain.PastFrostDKGStartedEvents( - &DKGStartedEventFilter{StartBlock: 0}, + &FrostDKGStartedEventFilter{StartBlock: startBlock}, ) if err != nil { logger.Errorf("failed to recover past FROST DKG started events: [%v]", err) @@ -186,8 +197,14 @@ func recoverFrostDKGCoordinatorState( ) case Challenge: + startBlock, err := frostDKGRecoveryStartBlock(node) + if err != nil { + logger.Errorf("failed to resolve FROST DKG recovery start block: [%v]", err) + return + } + events, err := frostChain.PastFrostDKGResultSubmittedEvents( - &DKGStartedEventFilter{StartBlock: 0}, + &FrostDKGResultSubmittedEventFilter{StartBlock: startBlock}, ) if err != nil { logger.Errorf("failed to recover past FROST DKG result submissions: [%v]", err) @@ -393,3 +410,25 @@ func localFrostMembership( return memberIndexes, groupSelectionResult, nil } + +func frostDKGRecoveryStartBlock(node *node) (uint64, error) { + blockCounter, err := node.chain.BlockCounter() + if err != nil { + return 0, err + } + + currentBlock, err := blockCounter.CurrentBlock() + if err != nil { + return 0, err + } + + return boundedFrostDKGRecoveryStartBlock(currentBlock), nil +} + +func boundedFrostDKGRecoveryStartBlock(currentBlock uint64) uint64 { + if currentBlock <= frostDKGRecoveryLookBackBlocks { + return 0 + } + + return currentBlock - frostDKGRecoveryLookBackBlocks +} diff --git a/pkg/tbtc/frost_dkg_execution_frost_native.go b/pkg/tbtc/frost_dkg_execution_frost_native.go index a80167abb4..f0921142eb 100644 --- a/pkg/tbtc/frost_dkg_execution_frost_native.go +++ b/pkg/tbtc/frost_dkg_execution_frost_native.go @@ -19,6 +19,8 @@ import ( "github.com/keep-network/keep-core/pkg/tecdsa" ) +const frostDKGResultSubmissionDelayStepBlocks = 30 + func executeFrostDKGIfPossible( ctx context.Context, node *node, @@ -66,8 +68,15 @@ func executeFrostDKGIfPossible( 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 + submitterMemberIndex := lowestMemberIndex(memberIndexes) for _, currentMemberIndex := range memberIndexes { memberIndex := currentMemberIndex @@ -111,7 +120,7 @@ func executeFrostDKGIfPossible( &frostsigning.NativeFROSTDKGRequest{ MemberIndex: memberIndex, GroupSize: len(groupSelectionResult.OperatorsIDs), - Threshold: node.groupParameters.GroupQuorum, + Threshold: signatureThreshold, SessionID: sessionID, IncludedMembersIndexes: activeMemberIndexes, Channel: channel, @@ -142,7 +151,7 @@ func executeFrostDKGIfPossible( } unsignedResult, err := registry.AssembleResult( - uint64(memberIndex), + uint64(submitterMemberIndex), outputKey, fullMembers, misbehavedMembersIndices, @@ -182,11 +191,20 @@ func executeFrostDKGIfPossible( 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, - memberIndex, + submitterMemberIndex, activeMemberIndexes, signedResult, ); err != nil { @@ -310,13 +328,20 @@ func submitFrostDKGResultWithDelay( activeMemberIndexes []group.MemberIndex, result *registry.Result, ) error { - rank := 0 + 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 { @@ -328,7 +353,8 @@ func submitFrostDKGResultWithDelay( return err } - submissionBlock := currentBlock + uint64(rank)*dkgResultSubmissionDelayStepBlocks + submissionBlock := currentBlock + + uint64(rank)*frostDKGResultSubmissionDelayStepBlocks if err := node.waitForBlockHeight(ctx, submissionBlock); err != nil { return err } @@ -409,6 +435,21 @@ func frostFullMembers( return members } +func lowestMemberIndex(memberIndexes []group.MemberIndex) group.MemberIndex { + if len(memberIndexes) == 0 { + return 0 + } + + lowest := memberIndexes[0] + for _, memberIndex := range memberIndexes[1:] { + if memberIndex < lowest { + lowest = memberIndex + } + } + + return lowest +} + func frostMisbehavedMemberIndices( groupSize int, activeMemberIndexes []group.MemberIndex, 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..1c31976943 --- /dev/null +++ b/pkg/tbtc/frost_dkg_execution_frost_native_test.go @@ -0,0 +1,43 @@ +//go:build frost_native + +package tbtc + +import ( + "testing" + + "github.com/keep-network/keep-core/pkg/frost/registry" + "github.com/keep-network/keep-core/pkg/protocol/group" +) + +func TestLowestMemberIndex(t *testing.T) { + actual := lowestMemberIndex([]group.MemberIndex{9, 3, 7}) + if actual != 3 { + t.Fatalf("unexpected lowest member index\nexpected: [3]\nactual: [%d]", 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], + ) + } + } +} 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..b35c1778e2 --- /dev/null +++ b/pkg/tbtc/frost_dkg_parameters_test.go @@ -0,0 +1,69 @@ +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) { + testCases := map[string]struct { + currentBlock uint64 + expected uint64 + }{ + "below lookback": { + currentBlock: 100, + expected: 0, + }, + "equal lookback": { + currentBlock: frostDKGRecoveryLookBackBlocks, + expected: 0, + }, + "above lookback": { + currentBlock: frostDKGRecoveryLookBackBlocks + 123, + expected: 123, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + actual := boundedFrostDKGRecoveryStartBlock(test.currentBlock) + if actual != test.expected { + t.Fatalf( + "unexpected start block\nexpected: [%d]\nactual: [%d]", + test.expected, + actual, + ) + } + }) + } +} diff --git a/pkg/tbtc/frost_dkg_result_signing.go b/pkg/tbtc/frost_dkg_result_signing.go index 9033c6bf22..5c80e7eca8 100644 --- a/pkg/tbtc/frost_dkg_result_signing.go +++ b/pkg/tbtc/frost_dkg_result_signing.go @@ -117,10 +117,13 @@ func signAndCollectFrostDKGResultSignatures( memberIndex: ownSignature, } - expectedSignaturesCount := node.groupParameters.GroupQuorum + 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 quorum [%d]", + "FROST DKG included members count [%d] is below signature threshold [%d]", len(includedMembersIndexes), expectedSignaturesCount, ) @@ -129,7 +132,12 @@ func signAndCollectFrostDKGResultSignatures( recvCtx, cancelRecvCtx := context.WithCancel(ctx) defer cancelRecvCtx() - messageChan := make(chan *frostDKGResultSignatureMessage, len(includedMembersIndexes)*4+1) + // 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 { From 916f91e7048a76d456471ac900964a729c1ad1dc Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 25 May 2026 13:46:48 -0500 Subject: [PATCH 4/6] fix(frost): harden DKG recovery and submitter election --- pkg/tbtc/frost_dkg_coordinator.go | 61 +++++++++++++++---- pkg/tbtc/frost_dkg_execution_frost_native.go | 39 +++++++++--- .../frost_dkg_execution_frost_native_test.go | 38 ++++++++++-- pkg/tbtc/frost_dkg_parameters_test.go | 42 ++++++++++++- 4 files changed, 153 insertions(+), 27 deletions(-) diff --git a/pkg/tbtc/frost_dkg_coordinator.go b/pkg/tbtc/frost_dkg_coordinator.go index 3690bb8576..135a574b92 100644 --- a/pkg/tbtc/frost_dkg_coordinator.go +++ b/pkg/tbtc/frost_dkg_coordinator.go @@ -7,11 +7,6 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/group" ) -// frostDKGRecoveryLookBackBlocks bounds cold-start eth_getLogs queries. -// FROST DKG recovery only needs recent AwaitingResult/Challenge events, and -// keeping the range at 10k blocks stays inside common RPC provider limits. -const frostDKGRecoveryLookBackBlocks = 10000 - func initializeFrostDKGCoordinator( ctx context.Context, node *node, @@ -169,7 +164,7 @@ func recoverFrostDKGCoordinatorState( switch state { case AwaitingResult: - startBlock, err := frostDKGRecoveryStartBlock(node) + startBlock, err := frostDKGRecoveryStartBlock(node, frostChain) if err != nil { logger.Errorf("failed to resolve FROST DKG recovery start block: [%v]", err) return @@ -197,7 +192,7 @@ func recoverFrostDKGCoordinatorState( ) case Challenge: - startBlock, err := frostDKGRecoveryStartBlock(node) + startBlock, err := frostDKGRecoveryStartBlock(node, frostChain) if err != nil { logger.Errorf("failed to resolve FROST DKG recovery start block: [%v]", err) return @@ -411,7 +406,10 @@ func localFrostMembership( return memberIndexes, groupSelectionResult, nil } -func frostDKGRecoveryStartBlock(node *node) (uint64, error) { +func frostDKGRecoveryStartBlock( + node *node, + frostChain FrostDKGChain, +) (uint64, error) { blockCounter, err := node.chain.BlockCounter() if err != nil { return 0, err @@ -422,13 +420,52 @@ func frostDKGRecoveryStartBlock(node *node) (uint64, error) { return 0, err } - return boundedFrostDKGRecoveryStartBlock(currentBlock), nil + 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) uint64 { - if currentBlock <= frostDKGRecoveryLookBackBlocks { +func boundedFrostDKGRecoveryStartBlock( + currentBlock uint64, + lookBackBlocks uint64, +) uint64 { + if currentBlock <= lookBackBlocks { return 0 } - return currentBlock - frostDKGRecoveryLookBackBlocks + return currentBlock - lookBackBlocks } diff --git a/pkg/tbtc/frost_dkg_execution_frost_native.go b/pkg/tbtc/frost_dkg_execution_frost_native.go index f0921142eb..0828f1e605 100644 --- a/pkg/tbtc/frost_dkg_execution_frost_native.go +++ b/pkg/tbtc/frost_dkg_execution_frost_native.go @@ -76,7 +76,6 @@ func executeFrostDKGIfPossible( fullMembers := frostFullMembers(groupSelectionResult) dkgTimeoutBlock := event.BlockNumber + params.SubmissionTimeoutBlocks - submitterMemberIndex := lowestMemberIndex(memberIndexes) for _, currentMemberIndex := range memberIndexes { memberIndex := currentMemberIndex @@ -114,6 +113,19 @@ func executeFrostDKGIfPossible( 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 + } + nativeResult, err := frostsigning.ExecuteNativeFROSTDKG( dkgCtx, dkgLogger, @@ -435,15 +447,26 @@ func frostFullMembers( return members } -func lowestMemberIndex(memberIndexes []group.MemberIndex) group.MemberIndex { - if len(memberIndexes) == 0 { - return 0 +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{}{} } - lowest := memberIndexes[0] - for _, memberIndex := range memberIndexes[1:] { - if memberIndex < lowest { - lowest = memberIndex + var lowest group.MemberIndex + for _, localMemberIndex := range localMemberIndexes { + if _, ok := activeMembersSet[localMemberIndex]; !ok { + continue + } + + if lowest == 0 || localMemberIndex < lowest { + lowest = localMemberIndex } } diff --git a/pkg/tbtc/frost_dkg_execution_frost_native_test.go b/pkg/tbtc/frost_dkg_execution_frost_native_test.go index 1c31976943..7b9deb785d 100644 --- a/pkg/tbtc/frost_dkg_execution_frost_native_test.go +++ b/pkg/tbtc/frost_dkg_execution_frost_native_test.go @@ -9,10 +9,40 @@ import ( "github.com/keep-network/keep-core/pkg/protocol/group" ) -func TestLowestMemberIndex(t *testing.T) { - actual := lowestMemberIndex([]group.MemberIndex{9, 3, 7}) - if actual != 3 { - t.Fatalf("unexpected lowest member index\nexpected: [3]\nactual: [%d]", actual) +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, + ) + } + }) } } diff --git a/pkg/tbtc/frost_dkg_parameters_test.go b/pkg/tbtc/frost_dkg_parameters_test.go index b35c1778e2..c97cd6158a 100644 --- a/pkg/tbtc/frost_dkg_parameters_test.go +++ b/pkg/tbtc/frost_dkg_parameters_test.go @@ -36,6 +36,8 @@ func TestFrostDKGSignatureThresholdRejectsInvalidParameters(t *testing.T) { } func TestBoundedFrostDKGRecoveryStartBlock(t *testing.T) { + lookBackBlocks := uint64(13560) + testCases := map[string]struct { currentBlock uint64 expected uint64 @@ -45,18 +47,25 @@ func TestBoundedFrostDKGRecoveryStartBlock(t *testing.T) { expected: 0, }, "equal lookback": { - currentBlock: frostDKGRecoveryLookBackBlocks, + currentBlock: lookBackBlocks, expected: 0, }, + "one block above lookback": { + currentBlock: lookBackBlocks + 1, + expected: 1, + }, "above lookback": { - currentBlock: frostDKGRecoveryLookBackBlocks + 123, + currentBlock: lookBackBlocks + 123, expected: 123, }, } for name, test := range testCases { t.Run(name, func(t *testing.T) { - actual := boundedFrostDKGRecoveryStartBlock(test.currentBlock) + actual := boundedFrostDKGRecoveryStartBlock( + test.currentBlock, + lookBackBlocks, + ) if actual != test.expected { t.Fatalf( "unexpected start block\nexpected: [%d]\nactual: [%d]", @@ -67,3 +76,30 @@ func TestBoundedFrostDKGRecoveryStartBlock(t *testing.T) { }) } } + +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, + ) + } +} From 0e947164e0295c18995d0098e72a6b3b3aea60e7 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 25 May 2026 17:27:04 -0500 Subject: [PATCH 5/6] fix(frost): harden DKG event recovery and challenges --- pkg/chain/ethereum/frost_dkg.go | 307 +++++++++++++++++++++---- pkg/tbtc/frost_dkg_coordinator.go | 130 ++++++++++- pkg/tbtc/frost_dkg_coordinator_test.go | 78 +++++++ 3 files changed, 462 insertions(+), 53 deletions(-) create mode 100644 pkg/tbtc/frost_dkg_coordinator_test.go diff --git a/pkg/chain/ethereum/frost_dkg.go b/pkg/chain/ethereum/frost_dkg.go index b09a980e36..ddadabb0a5 100644 --- a/pkg/chain/ethereum/frost_dkg.go +++ b/pkg/chain/ethereum/frost_dkg.go @@ -5,9 +5,12 @@ import ( "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" @@ -45,41 +48,48 @@ func (tc *TbtcChain) OnFrostDKGStarted( } ctx, cancelCtx := context.WithCancel(context.Background()) - sink := make(chan *frostabi.FrostWalletRegistryDkgStarted) - - sub, err := tc.frostWalletRegistry.WatchDkgStarted( - &bind.WatchOpts{Context: ctx}, - sink, - nil, - ) - if err != nil { - cancelCtx() - logger.Errorf("failed to watch FROST DKG started events: [%v]", err) - return subscription.NewEventSubscription(func() {}) - } + events := make(chan *tbtc.FrostDKGStartedEvent) + watchSink := make(chan *frostabi.FrostWalletRegistryDkgStarted) go func() { for { select { case <-ctx.Done(): return - case err, ok := <-sub.Err(): + case event, ok := <-events: if !ok { return } - logger.Errorf("FROST DKG started subscription error: [%v]", err) - case event, ok := <-sink: + handler(event) + } + } + }() + + go func() { + for { + select { + case <-ctx.Done(): + return + case event, ok := <-watchSink: if !ok { return } - handler(&tbtc.FrostDKGStartedEvent{ - Seed: event.Seed, - BlockNumber: event.Raw.BlockNumber, - }) + 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() @@ -143,31 +153,29 @@ func (tc *TbtcChain) OnFrostDKGResultSubmitted( } ctx, cancelCtx := context.WithCancel(context.Background()) - sink := make(chan *frostabi.FrostWalletRegistryDkgResultSubmitted) - - sub, err := tc.frostWalletRegistry.WatchDkgResultSubmitted( - &bind.WatchOpts{Context: ctx}, - sink, - nil, - nil, - ) - if err != nil { - cancelCtx() - logger.Errorf("failed to watch FROST DKG result submitted events: [%v]", err) - return subscription.NewEventSubscription(func() {}) - } + events := make(chan *tbtc.FrostDKGResultSubmittedEvent) + watchSink := make(chan *frostabi.FrostWalletRegistryDkgResultSubmitted) go func() { for { select { case <-ctx.Done(): return - case err, ok := <-sub.Err(): + case event, ok := <-events: if !ok { return } - logger.Errorf("FROST DKG result submitted subscription error: [%v]", err) - case event, ok := <-sink: + handler(event) + } + } + }() + + go func() { + for { + select { + case <-ctx.Done(): + return + case event, ok := <-watchSink: if !ok { return } @@ -177,16 +185,24 @@ func (tc *TbtcChain) OnFrostDKGResultSubmitted( continue } - handler(&tbtc.FrostDKGResultSubmittedEvent{ - Seed: event.Seed, - ResultHash: event.ResultHash, - Result: result, - BlockNumber: event.Raw.BlockNumber, - }) + 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() @@ -253,6 +269,215 @@ func (tc *TbtcChain) PastFrostDKGResultSubmittedEvents( 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), diff --git a/pkg/tbtc/frost_dkg_coordinator.go b/pkg/tbtc/frost_dkg_coordinator.go index 135a574b92..dfea4188b6 100644 --- a/pkg/tbtc/frost_dkg_coordinator.go +++ b/pkg/tbtc/frost_dkg_coordinator.go @@ -258,13 +258,7 @@ func handleFrostDKGResultSubmitted( event.ResultHash, reason, ) - if err := frostChain.ChallengeFrostDKGResult(event.Result); err != nil { - logger.Errorf( - "failed to challenge FROST DKG result [0x%x]: [%v]", - event.ResultHash, - err, - ) - } + challengeInvalidFrostDKGResult(ctx, node, frostChain, event) return } @@ -314,6 +308,114 @@ func handleFrostDKGResultSubmitted( } } +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, @@ -406,16 +508,20 @@ func localFrostMembership( return memberIndexes, groupSelectionResult, nil } -func frostDKGRecoveryStartBlock( - node *node, - frostChain FrostDKGChain, -) (uint64, error) { +func currentFrostDKGBlock(node *node) (uint64, error) { blockCounter, err := node.chain.BlockCounter() if err != nil { return 0, err } - currentBlock, err := blockCounter.CurrentBlock() + return blockCounter.CurrentBlock() +} + +func frostDKGRecoveryStartBlock( + node *node, + frostChain FrostDKGChain, +) (uint64, error) { + currentBlock, err := currentFrostDKGBlock(node) if err != nil { return 0, err } 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 +} From dce62d72f0d7b7fe66fc4f6f7a486eecf446b087 Mon Sep 17 00:00:00 2001 From: maclane Date: Mon, 25 May 2026 17:44:28 -0500 Subject: [PATCH 6/6] docs(frost): note DKG digest wire-format coupling --- docs/development/frost-roast-retry-rollout.adoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/development/frost-roast-retry-rollout.adoc b/docs/development/frost-roast-retry-rollout.adoc index 610fbfe6ea..ee920a555d 100644 --- a/docs/development/frost-roast-retry-rollout.adoc +++ b/docs/development/frost-roast-retry-rollout.adoc @@ -118,6 +118,20 @@ RFC-21 design and is enforced in 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`