From 39f821d23a21d28dc29775c6337a12aa4918348c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ros=C5=82aniec?=
Date: Thu, 23 Jul 2026 07:23:59 +0000
Subject: [PATCH] refactor(ethereum): split tBTC chain adapter into per-concern
files
pkg/chain/ethereum/tbtc.go had grown to ~2400 lines and ~90 methods
spanning every tBTC concern. Split it into per-concern files within the
same package so the adapter is navigable:
- tbtc.go: TbtcChain struct, constructor, shared constants
- tbtc_sortition.go: sortition pool, operator status, group selection
- tbtc_dkg.go: DKG lifecycle, result assembly and validation
- tbtc_inactivity.go: inactivity claims
- tbtc_deposit.go: deposit reveal, sweep proposals and proofs
- tbtc_redemption.go: redemption requests, proposals and proofs
- tbtc_wallet.go: wallet registration/state, heartbeat proposals
- tbtc_moving_funds.go: moving funds and moved funds sweep
Pure relocation: no declaration bodies changed. Verified by comparing
the AST of every top-level declaration before and after the split - all
96 declarations are byte-identical.
---
pkg/chain/ethereum/tbtc.go | 2145 -----------------------
pkg/chain/ethereum/tbtc_deposit.go | 310 ++++
pkg/chain/ethereum/tbtc_dkg.go | 555 ++++++
pkg/chain/ethereum/tbtc_inactivity.go | 195 +++
pkg/chain/ethereum/tbtc_moving_funds.go | 408 +++++
pkg/chain/ethereum/tbtc_redemption.go | 297 ++++
pkg/chain/ethereum/tbtc_sortition.go | 247 +++
pkg/chain/ethereum/tbtc_wallet.go | 236 +++
8 files changed, 2248 insertions(+), 2145 deletions(-)
create mode 100644 pkg/chain/ethereum/tbtc_deposit.go
create mode 100644 pkg/chain/ethereum/tbtc_dkg.go
create mode 100644 pkg/chain/ethereum/tbtc_inactivity.go
create mode 100644 pkg/chain/ethereum/tbtc_moving_funds.go
create mode 100644 pkg/chain/ethereum/tbtc_redemption.go
create mode 100644 pkg/chain/ethereum/tbtc_sortition.go
create mode 100644 pkg/chain/ethereum/tbtc_wallet.go
diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go
index 50e42be20e..8a0e83d4ae 100644
--- a/pkg/chain/ethereum/tbtc.go
+++ b/pkg/chain/ethereum/tbtc.go
@@ -1,38 +1,19 @@
package ethereum
import (
- "context"
- "crypto/ecdsa"
- "encoding/binary"
"errors"
"fmt"
"math/big"
- "reflect"
- "sort"
"time"
"github.com/keep-network/keep-common/pkg/cache"
- "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
- "github.com/ethereum/go-ethereum/crypto"
- "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil"
- "github.com/keep-network/keep-core/pkg/bitcoin"
"github.com/keep-network/keep-common/pkg/chain/ethereum"
- "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"
- 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/crypto/secp256k1"
- "github.com/keep-network/keep-core/pkg/internal/byteutils"
- "github.com/keep-network/keep-core/pkg/operator"
- "github.com/keep-network/keep-core/pkg/protocol/group"
- "github.com/keep-network/keep-core/pkg/protocol/inactivity"
- "github.com/keep-network/keep-core/pkg/subscription"
"github.com/keep-network/keep-core/pkg/tbtc"
- "github.com/keep-network/keep-core/pkg/tecdsa/dkg"
)
// Definitions of contract names.
@@ -279,2132 +260,6 @@ func newTbtcChain(
}, nil
}
-// EcdsaWalletGroupParametersFromChain mirrors EcdsaDkgValidator sizing constants
-// when EcdsaDkgValidator contract address was configured under [ethereum]
-// contract addresses or developer.ecdsaDkgValidatorAddress alias. When absent,
-// returns (nil, nil) and callers use defaultGroupParameters(network).
-func (tc *TbtcChain) EcdsaWalletGroupParametersFromChain(
- ctx context.Context,
-) (*tbtc.GroupParameters, error) {
- if tc.ecdsaDkgValidatorAddress == (common.Address{}) {
- return nil, nil
- }
- return ecdsaWalletGroupParametersFromValidator(
- ctx,
- tc.baseChain.client,
- tc.ecdsaDkgValidatorAddress,
- )
-}
-
-// Staking returns address of the TokenStaking contract the WalletRegistry is
-// connected to.
-func (tc *TbtcChain) Staking() (chain.Address, error) {
- stakingContractAddress, err := tc.walletRegistry.Staking()
- if err != nil {
- return "", fmt.Errorf(
- "failed to get the token staking address: [%w]",
- err,
- )
- }
-
- return chain.Address(stakingContractAddress.String()), nil
-}
-
-// IsRecognized checks whether the given operator is recognized by the TbtcChain
-// as eligible to join the network. If the operator has a stake delegation or
-// had a stake delegation in the past, it will be recognized.
-func (tc *TbtcChain) IsRecognized(operatorPublicKey *operator.PublicKey) (bool, error) {
- operatorAddress, err := operatorPublicKeyToChainAddress(operatorPublicKey)
- if err != nil {
- return false, fmt.Errorf(
- "cannot convert from operator key to chain address: [%v]",
- err,
- )
- }
-
- stakingProvider, err := tc.walletRegistry.OperatorToStakingProvider(
- operatorAddress,
- )
- if err != nil {
- return false, fmt.Errorf(
- "failed to map operator [%v] to a staking provider: [%v]",
- operatorAddress,
- err,
- )
- }
-
- if (stakingProvider == common.Address{}) {
- return false, nil
- }
-
- // Check if the staking provider has an owner. This check ensures that there
- // is/was a stake delegation for the given staking provider.
- _, _, _, hasStakeDelegation, err := tc.baseChain.RolesOf(
- chain.Address(stakingProvider.Hex()),
- )
- if err != nil {
- return false, fmt.Errorf(
- "failed to check stake delegation for staking provider [%v]: [%v]",
- stakingProvider,
- err,
- )
- }
-
- if !hasStakeDelegation {
- return false, nil
- }
-
- return true, nil
-}
-
-// OperatorToStakingProvider returns the staking provider address for the
-// operator. If the staking provider has not been registered for the
-// operator, the returned address is empty and the boolean flag is set to
-// false. If the staking provider has been registered, the address is not
-// empty and the boolean flag indicates true.
-func (tc *TbtcChain) OperatorToStakingProvider() (chain.Address, bool, error) {
- stakingProvider, err := tc.walletRegistry.OperatorToStakingProvider(tc.key.Address)
- if err != nil {
- return "", false, fmt.Errorf(
- "failed to map operator [%v] to a staking provider: [%v]",
- tc.key.Address,
- err,
- )
- }
-
- if (stakingProvider == common.Address{}) {
- return "", false, nil
- }
-
- return chain.Address(stakingProvider.Hex()), true, nil
-}
-
-// EligibleStake returns the current value of the staking provider's
-// eligible stake. Eligible stake is defined as the currently authorized
-// stake minus the pending authorization decrease. Eligible stake
-// is what is used for operator's weight in the sortition pool.
-// If the authorized stake minus the pending authorization decrease
-// is below the minimum authorization, eligible stake is 0.
-func (tc *TbtcChain) EligibleStake(stakingProvider chain.Address) (*big.Int, error) {
- eligibleStake, err := tc.walletRegistry.EligibleStake(
- common.HexToAddress(stakingProvider.String()),
- )
- if err != nil {
- return nil, fmt.Errorf(
- "failed to get eligible stake for staking provider %s: [%w]",
- stakingProvider,
- err,
- )
- }
-
- return eligibleStake, nil
-}
-
-// IsPoolLocked returns true if the sortition pool is locked and no state
-// changes are allowed.
-func (tc *TbtcChain) IsPoolLocked() (bool, error) {
- return tc.sortitionPool.IsLocked()
-}
-
-// IsOperatorInPool returns true if the operator is registered in
-// the sortition pool.
-func (tc *TbtcChain) IsOperatorInPool() (bool, error) {
- return tc.walletRegistry.IsOperatorInPool(tc.key.Address)
-}
-
-// IsOperatorUpToDate checks if the operator's authorized stake is in sync
-// with operator's weight in the sortition pool.
-// If the operator's authorized stake is not in sync with sortition pool
-// weight, function returns false.
-// If the operator is not in the sortition pool and their authorized stake
-// is non-zero, function returns false.
-func (tc *TbtcChain) IsOperatorUpToDate() (bool, error) {
- return tc.walletRegistry.IsOperatorUpToDate(tc.key.Address)
-}
-
-// JoinSortitionPool executes a transaction to have the operator join the
-// sortition pool.
-func (tc *TbtcChain) JoinSortitionPool() error {
- _, err := tc.walletRegistry.JoinSortitionPool()
- return err
-}
-
-// UpdateOperatorStatus executes a transaction to update the operator's
-// state in the sortition pool.
-func (tc *TbtcChain) UpdateOperatorStatus() error {
- _, err := tc.walletRegistry.UpdateOperatorStatus(tc.key.Address)
- return err
-}
-
-// IsEligibleForRewards checks whether the operator is eligible for rewards
-// or not.
-func (tc *TbtcChain) IsEligibleForRewards() (bool, error) {
- return tc.sortitionPool.IsEligibleForRewards(tc.key.Address)
-}
-
-// Checks whether the operator is able to restore their eligibility for
-// rewards right away.
-func (tc *TbtcChain) CanRestoreRewardEligibility() (bool, error) {
- return tc.sortitionPool.CanRestoreRewardEligibility(tc.key.Address)
-}
-
-// Restores reward eligibility for the operator.
-func (tc *TbtcChain) RestoreRewardEligibility() error {
- _, err := tc.sortitionPool.RestoreRewardEligibility(tc.key.Address)
- return err
-}
-
-// Returns true if the chaosnet phase is active, false otherwise.
-func (tc *TbtcChain) IsChaosnetActive() (bool, error) {
- return tc.sortitionPool.IsChaosnetActive()
-}
-
-// Returns true if operator is a beta operator, false otherwise.
-// Chaosnet status does not matter.
-func (tc *TbtcChain) IsBetaOperator() (bool, error) {
- return tc.sortitionPool.IsBetaOperator(tc.key.Address)
-}
-
-// GetOperatorID returns the ID number of the given operator address. An ID
-// number of 0 means the operator has not been allocated an ID number yet.
-func (tc *TbtcChain) GetOperatorID(
- operatorAddress chain.Address,
-) (chain.OperatorID, error) {
- return tc.sortitionPool.GetOperatorID(
- common.HexToAddress(operatorAddress.String()),
- )
-}
-
-// SelectGroup returns the group members selected for the current group
-// selection. The function returns an error if the chain's state does not allow
-// for group selection at the moment.
-func (tc *TbtcChain) SelectGroup() (*tbtc.GroupSelectionResult, error) {
- operatorsIDs, err := tc.walletRegistry.SelectGroup()
- if err != nil {
- return nil, fmt.Errorf(
- "cannot select group in the sortition pool: [%v]",
- err,
- )
- }
-
- operatorsAddresses, err := tc.sortitionPool.GetIDOperators(operatorsIDs)
- if err != nil {
- return nil, fmt.Errorf(
- "cannot convert operators' IDs to addresses: [%v]",
- err,
- )
- }
-
- // Should not happen as this is guaranteed by the contract but, just in case.
- if len(operatorsIDs) != len(operatorsAddresses) {
- return nil, fmt.Errorf("operators IDs and addresses mismatch")
- }
-
- 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
-}
-
-func (tc *TbtcChain) OnDKGStarted(
- handler func(event *tbtc.DKGStartedEvent),
-) subscription.EventSubscription {
- onEvent := func(
- seed *big.Int,
- blockNumber uint64,
- ) {
- handler(&tbtc.DKGStartedEvent{
- Seed: seed,
- BlockNumber: blockNumber,
- })
- }
-
- return tc.walletRegistry.DkgStartedEvent(nil, nil).OnEvent(onEvent)
-}
-
-func (tc *TbtcChain) PastDKGStartedEvents(
- filter *tbtc.DKGStartedEventFilter,
-) ([]*tbtc.DKGStartedEvent, error) {
- var startBlock uint64
- var endBlock *uint64
- var seed []*big.Int
-
- if filter != nil {
- startBlock = filter.StartBlock
- endBlock = filter.EndBlock
- seed = filter.Seed
- }
-
- events, err := tc.walletRegistry.PastDkgStartedEvents(
- startBlock,
- endBlock,
- seed,
- )
- if err != nil {
- return nil, err
- }
-
- dkgStartedEvents := make([]*tbtc.DKGStartedEvent, len(events))
- for i, event := range events {
- dkgStartedEvents[i] = &tbtc.DKGStartedEvent{
- Seed: event.Seed,
- BlockNumber: event.Raw.BlockNumber,
- }
- }
-
- sort.SliceStable(dkgStartedEvents, func(i, j int) bool {
- return dkgStartedEvents[i].BlockNumber < dkgStartedEvents[j].BlockNumber
- })
-
- return dkgStartedEvents, err
-}
-
-func (tc *TbtcChain) OnDKGResultSubmitted(
- handler func(event *tbtc.DKGResultSubmittedEvent),
-) subscription.EventSubscription {
- onEvent := func(
- resultHash [32]byte,
- seed *big.Int,
- result ecdsaabi.EcdsaDkgResult,
- blockNumber uint64,
- ) {
- tbtcResult, err := convertDkgResultFromAbiType(result)
- if err != nil {
- logger.Errorf(
- "unexpected DKG result in DKGResultSubmitted event: [%v]",
- err,
- )
- return
- }
-
- handler(&tbtc.DKGResultSubmittedEvent{
- Seed: seed,
- ResultHash: resultHash,
- Result: tbtcResult,
- BlockNumber: blockNumber,
- })
- }
-
- return tc.walletRegistry.
- DkgResultSubmittedEvent(nil, nil, nil).
- OnEvent(onEvent)
-}
-
-// convertDkgResultFromAbiType converts the WalletRegistry-specific DKG
-// result to the format applicable for the TBTC application.
-func convertDkgResultFromAbiType(
- result ecdsaabi.EcdsaDkgResult,
-) (*tbtc.DKGChainResult, error) {
- if err := validateMemberIndex(result.SubmitterMemberIndex); err != nil {
- return nil, fmt.Errorf(
- "unexpected submitter member index: [%v]",
- err,
- )
- }
-
- signingMembersIndexes := make(
- []group.MemberIndex,
- len(result.SigningMembersIndices),
- )
- for i, memberIndex := range result.SigningMembersIndices {
- if err := validateMemberIndex(memberIndex); err != nil {
- return nil, fmt.Errorf(
- "unexpected signing member index: [%v]",
- err,
- )
- }
-
- signingMembersIndexes[i] = group.MemberIndex(memberIndex.Uint64())
- }
-
- return &tbtc.DKGChainResult{
- SubmitterMemberIndex: group.MemberIndex(result.SubmitterMemberIndex.Uint64()),
- GroupPublicKey: result.GroupPubKey,
- MisbehavedMembersIndexes: result.MisbehavedMembersIndices,
- Signatures: result.Signatures,
- SigningMembersIndexes: signingMembersIndexes,
- Members: result.Members,
- MembersHash: result.MembersHash,
- }, nil
-}
-
-// convertDkgResultToAbiType converts the TBTC-specific DKG result to
-// the format applicable for the WalletRegistry ABI.
-func convertDkgResultToAbiType(
- result *tbtc.DKGChainResult,
-) ecdsaabi.EcdsaDkgResult {
- signingMembersIndices := make([]*big.Int, len(result.SigningMembersIndexes))
- for i, memberIndex := range result.SigningMembersIndexes {
- signingMembersIndices[i] = big.NewInt(int64(memberIndex))
- }
-
- return ecdsaabi.EcdsaDkgResult{
- SubmitterMemberIndex: big.NewInt(int64(result.SubmitterMemberIndex)),
- GroupPubKey: result.GroupPublicKey,
- MisbehavedMembersIndices: result.MisbehavedMembersIndexes,
- Signatures: result.Signatures,
- SigningMembersIndices: signingMembersIndices,
- Members: result.Members,
- MembersHash: result.MembersHash,
- }
-}
-
-func validateMemberIndex(chainMemberIndex *big.Int) error {
- maxMemberIndex := big.NewInt(group.MaxMemberIndex)
- if chainMemberIndex.Cmp(maxMemberIndex) > 0 {
- return fmt.Errorf("invalid member index value: [%v]", chainMemberIndex)
- }
-
- return nil
-}
-
-func (tc *TbtcChain) OnDKGResultChallenged(
- handler func(event *tbtc.DKGResultChallengedEvent),
-) subscription.EventSubscription {
- onEvent := func(
- resultHash [32]byte,
- challenger common.Address,
- reason string,
- blockNumber uint64,
- ) {
- handler(&tbtc.DKGResultChallengedEvent{
- ResultHash: resultHash,
- Challenger: chain.Address(challenger.Hex()),
- Reason: reason,
- BlockNumber: blockNumber,
- })
- }
-
- return tc.walletRegistry.
- DkgResultChallengedEvent(nil, nil, nil).
- OnEvent(onEvent)
-}
-
-func (tc *TbtcChain) OnDKGResultApproved(
- handler func(event *tbtc.DKGResultApprovedEvent),
-) subscription.EventSubscription {
- onEvent := func(
- resultHash [32]byte,
- approver common.Address,
- blockNumber uint64,
- ) {
- handler(&tbtc.DKGResultApprovedEvent{
- ResultHash: resultHash,
- Approver: chain.Address(approver.Hex()),
- BlockNumber: blockNumber,
- })
- }
-
- return tc.walletRegistry.
- DkgResultApprovedEvent(nil, nil, nil).
- OnEvent(onEvent)
-}
-
-// AssembleDKGResult assembles the DKG chain result according to the rules
-// expected by the given chain.
-func (tc *TbtcChain) AssembleDKGResult(
- submitterMemberIndex group.MemberIndex,
- groupPublicKey *ecdsa.PublicKey,
- operatingMembersIndexes []group.MemberIndex,
- misbehavedMembersIndexes []group.MemberIndex,
- signatures map[group.MemberIndex][]byte,
- groupSelectionResult *tbtc.GroupSelectionResult,
-) (*tbtc.DKGChainResult, error) {
- serializedGroupPublicKey, err := convertPubKeyToChainFormat(groupPublicKey)
- if err != nil {
- return nil, fmt.Errorf(
- "could not convert group public key to chain format: [%v]",
- err,
- )
- }
-
- // Sort misbehavedMembersIndexes slice in ascending order as expected
- // by the on-chain contract.
- sort.Slice(misbehavedMembersIndexes[:], func(i, j int) bool {
- return misbehavedMembersIndexes[i] < misbehavedMembersIndexes[j]
- })
-
- signingMemberIndices, signatureBytes, err := convertSignaturesToChainFormat(
- signatures,
- )
- if err != nil {
- return nil, fmt.Errorf(
- "could not convert signatures to chain format: [%v]",
- err,
- )
- }
-
- // Sort operatingOperatorsIDs slice in ascending order as the slice
- // holding the operators IDs used to compute the members hash is
- // expected to be sorted in the same way.
- sort.Slice(operatingMembersIndexes[:], func(i, j int) bool {
- return operatingMembersIndexes[i] < operatingMembersIndexes[j]
- })
-
- operatingOperatorsIDs := make([]chain.OperatorID, len(operatingMembersIndexes))
- for i, operatingMemberIndex := range operatingMembersIndexes {
- operatingOperatorsIDs[i] =
- groupSelectionResult.OperatorsIDs[operatingMemberIndex-1]
- }
-
- membersHash, err := computeOperatorsIDsHash(operatingOperatorsIDs)
- if err != nil {
- return nil, fmt.Errorf("could not compute members hash: [%v]", err)
- }
-
- return &tbtc.DKGChainResult{
- SubmitterMemberIndex: submitterMemberIndex,
- GroupPublicKey: serializedGroupPublicKey[:],
- MisbehavedMembersIndexes: misbehavedMembersIndexes,
- Signatures: signatureBytes,
- SigningMembersIndexes: signingMemberIndices,
- Members: groupSelectionResult.OperatorsIDs,
- MembersHash: membersHash,
- }, nil
-}
-
-func (tc *TbtcChain) SubmitDKGResult(
- dkgResult *tbtc.DKGChainResult,
-) error {
- _, err := tc.walletRegistry.SubmitDkgResult(
- convertDkgResultToAbiType(dkgResult),
- )
-
- return err
-}
-
-// computeOperatorsIDsHash computes the keccak256 hash for the given list
-// of operators IDs.
-func computeOperatorsIDsHash(operatorsIDs chain.OperatorIDs) ([32]byte, error) {
- uint32SliceType, err := abi.NewType("uint32[]", "uint32[]", nil)
- if err != nil {
- return [32]byte{}, err
- }
-
- bytes, err := abi.Arguments{{Type: uint32SliceType}}.Pack(operatorsIDs)
- if err != nil {
- return [32]byte{}, err
- }
-
- return crypto.Keccak256Hash(bytes), nil
-}
-
-// convertSignaturesToChainFormat converts signatures map to two slices. The
-// first slice contains indices of members from the map, sorted in ascending order
-// as required by the contract. The second slice is a slice of concatenated
-// signatures. Signatures and member indices are returned in the matching order.
-// It requires each signature to be exactly 65-byte long.
-func convertSignaturesToChainFormat(
- signatures map[group.MemberIndex][]byte,
-) ([]group.MemberIndex, []byte, error) {
- membersIndexes := make([]group.MemberIndex, 0)
- for memberIndex := range signatures {
- membersIndexes = append(membersIndexes, memberIndex)
- }
-
- sort.Slice(membersIndexes, func(i, j int) bool {
- return membersIndexes[i] < membersIndexes[j]
- })
-
- signatureSize := 65
-
- var signaturesSlice []byte
-
- for _, memberIndex := range membersIndexes {
- signature := signatures[memberIndex]
-
- if len(signature) != signatureSize {
- return nil, nil, fmt.Errorf(
- "invalid signature size for member [%v] got [%d] bytes but [%d] bytes required",
- memberIndex,
- len(signature),
- signatureSize,
- )
- }
-
- signaturesSlice = append(signaturesSlice, signature...)
- }
-
- return membersIndexes, signaturesSlice, nil
-}
-
-// convertPubKeyToChainFormat takes X and Y coordinates of a signer's public key
-// and concatenates it to a 64-byte long array. If any of coordinates is shorter
-// than 32-byte it is preceded with zeros.
-func convertPubKeyToChainFormat(publicKey *ecdsa.PublicKey) ([64]byte, error) {
- var serialized [64]byte
-
- x, err := byteutils.LeftPadTo32Bytes(publicKey.X.Bytes())
- if err != nil {
- return serialized, err
- }
-
- y, err := byteutils.LeftPadTo32Bytes(publicKey.Y.Bytes())
- if err != nil {
- return serialized, err
- }
-
- serializedBytes := append(x, y...)
-
- copy(serialized[:], serializedBytes)
-
- return serialized, nil
-}
-
-func (tc *TbtcChain) GetDKGState() (tbtc.DKGState, error) {
- walletCreationState, err := tc.walletRegistry.GetWalletCreationState()
- if err != nil {
- return 0, err
- }
-
- var state tbtc.DKGState
-
- switch walletCreationState {
- case 0:
- state = tbtc.Idle
- case 1:
- state = tbtc.AwaitingSeed
- case 2:
- state = tbtc.AwaitingResult
- case 3:
- state = tbtc.Challenge
- default:
- err = fmt.Errorf(
- "unexpected wallet creation state: [%v]",
- walletCreationState,
- )
- }
-
- return state, err
-}
-
-// CalculateDKGResultSignatureHash calculates a 32-byte hash that is used
-// to produce a signature supporting the given groupPublicKey computed
-// as result of the given DKG process. The misbehavedMembersIndexes parameter
-// should contain indexes of members that were considered as misbehaved
-// during the DKG process. The startBlock argument is the block at which
-// the given DKG process started.
-func (tc *TbtcChain) CalculateDKGResultSignatureHash(
- groupPublicKey *ecdsa.PublicKey,
- misbehavedMembersIndexes []group.MemberIndex,
- startBlock uint64,
-) (dkg.ResultSignatureHash, error) {
- groupPublicKeyBytes := secp256k1.Marshal(groupPublicKey)
- // Crop the 04 prefix as the calculateDKGResultSignatureHash function
- // expects an unprefixed 64-byte public key,
- unprefixedGroupPublicKeyBytes := groupPublicKeyBytes[1:]
-
- // Sort misbehavedMembersIndexes slice in ascending order as expected
- // by the calculateDKGResultSignatureHash function.
- sort.Slice(misbehavedMembersIndexes[:], func(i, j int) bool {
- return misbehavedMembersIndexes[i] < misbehavedMembersIndexes[j]
- })
-
- return calculateDKGResultSignatureHash(
- tc.chainID,
- unprefixedGroupPublicKeyBytes,
- misbehavedMembersIndexes,
- big.NewInt(int64(startBlock)),
- )
-}
-
-// calculateDKGResultSignatureHash computes the keccak256 hash for the given DKG
-// result parameters. It expects that the groupPublicKey is a 64-byte uncompressed
-// public key without the 04 prefix and misbehavedMembersIndexes slice is
-// sorted in ascending order. Those expectations are forced by the contract.
-func calculateDKGResultSignatureHash(
- chainID *big.Int,
- groupPublicKey []byte,
- misbehavedMembersIndexes []group.MemberIndex,
- startBlock *big.Int,
-) (dkg.ResultSignatureHash, error) {
- publicKeySize := 64
-
- if len(groupPublicKey) != publicKeySize {
- return dkg.ResultSignatureHash{}, fmt.Errorf(
- "wrong group public key length",
- )
- }
-
- uint256Type, err := abi.NewType("uint256", "uint256", nil)
- if err != nil {
- return dkg.ResultSignatureHash{}, err
- }
- bytesType, err := abi.NewType("bytes", "bytes", nil)
- if err != nil {
- return dkg.ResultSignatureHash{}, err
- }
- uint8SliceType, err := abi.NewType("uint8[]", "uint8[]", nil)
- if err != nil {
- return dkg.ResultSignatureHash{}, err
- }
-
- bytes, err := abi.Arguments{
- {Type: uint256Type},
- {Type: bytesType},
- {Type: uint8SliceType},
- {Type: uint256Type},
- }.Pack(
- chainID,
- groupPublicKey,
- misbehavedMembersIndexes,
- startBlock,
- )
- if err != nil {
- return dkg.ResultSignatureHash{}, err
- }
-
- return dkg.ResultSignatureHash(crypto.Keccak256Hash(bytes)), nil
-}
-
-func (tc *TbtcChain) IsDKGResultValid(
- dkgResult *tbtc.DKGChainResult,
-) (bool, error) {
- outcome, err := tc.walletRegistry.IsDkgResultValid(
- convertDkgResultToAbiType(dkgResult),
- )
- if err != nil {
- return false, fmt.Errorf("cannot check result validity: [%v]", err)
- }
-
- return parseDkgResultValidationOutcome(&outcome)
-}
-
-// parseDkgResultValidationOutcome parses the DKG validation outcome and returns
-// a boolean indicating whether the result is valid or not. The outcome parameter
-// must be a pointer to a struct containing a boolean flag as the first field.
-//
-// TODO: Find a better way to get the validity flag. This would require changes
-// in the contracts binding generator.
-func parseDkgResultValidationOutcome(
- outcome interface{},
-) (bool, error) {
- value := reflect.ValueOf(outcome)
- switch value.Kind() {
- case reflect.Pointer:
- default:
- return false, fmt.Errorf("result validation outcome is not a pointer")
- }
-
- field := value.Elem().Field(0)
- switch field.Kind() {
- case reflect.Bool:
- return field.Bool(), nil
- default:
- return false, fmt.Errorf("cannot parse result validation outcome")
- }
-}
-
-func (tc *TbtcChain) ChallengeDKGResult(dkgResult *tbtc.DKGChainResult) error {
- _, err := tc.walletRegistry.ChallengeDkgResult(
- convertDkgResultToAbiType(dkgResult),
- )
-
- return err
-}
-
-func (tc *TbtcChain) ApproveDKGResult(dkgResult *tbtc.DKGChainResult) error {
- result := convertDkgResultToAbiType(dkgResult)
-
- gasEstimate, err := tc.walletRegistry.ApproveDkgResultGasEstimate(result)
- if err != nil {
- return err
- }
-
- // The original estimate for this contract call turned out to be too low.
- // Here we add a 20% margin to overcome the gas problems.
- gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
-
- _, err = tc.walletRegistry.ApproveDkgResult(
- result,
- ethutil.TransactionOptions{
- GasLimit: uint64(gasEstimateWithMargin),
- },
- )
-
- return err
-}
-
-func (tc *TbtcChain) DKGParameters() (*tbtc.DKGParameters, error) {
- parameters, err := tc.walletRegistry.DkgParameters()
- if err != nil {
- return nil, err
- }
-
- return &tbtc.DKGParameters{
- SubmissionTimeoutBlocks: parameters.ResultSubmissionTimeout.Uint64(),
- ChallengePeriodBlocks: parameters.ResultChallengePeriodLength.Uint64(),
- ApprovePrecedencePeriodBlocks: parameters.SubmitterPrecedencePeriodLength.Uint64(),
- }, nil
-}
-
-func (tc *TbtcChain) OnInactivityClaimed(
- handler func(event *tbtc.InactivityClaimedEvent),
-) subscription.EventSubscription {
- onEvent := func(
- walletID [32]byte,
- nonce *big.Int,
- notifier common.Address,
- blockNumber uint64,
- ) {
- handler(&tbtc.InactivityClaimedEvent{
- WalletID: walletID,
- Nonce: nonce,
- Notifier: chain.Address(notifier.Hex()),
- BlockNumber: blockNumber,
- })
- }
-
- return tc.walletRegistry.InactivityClaimedEvent(nil, nil).OnEvent(onEvent)
-}
-
-func (tc *TbtcChain) AssembleInactivityClaim(
- walletID [32]byte,
- inactiveMembersIndices []group.MemberIndex,
- signatures map[group.MemberIndex][]byte,
- heartbeatFailed bool,
-) (
- *tbtc.InactivityClaim,
- error,
-) {
- signingMemberIndices, signatureBytes, err := convertSignaturesToChainFormat(
- signatures,
- )
- if err != nil {
- return nil, fmt.Errorf(
- "could not convert signatures to chain format: [%v]",
- err,
- )
- }
-
- return &tbtc.InactivityClaim{
- WalletID: walletID,
- InactiveMembersIndices: inactiveMembersIndices,
- HeartbeatFailed: heartbeatFailed,
- Signatures: signatureBytes,
- SigningMembersIndices: signingMemberIndices,
- }, nil
-}
-
-// convertInactivityClaimToAbiType converts the TBTC-specific inactivity claim
-// to the format applicable for the WalletRegistry ABI.
-func convertInactivityClaimToAbiType(
- claim *tbtc.InactivityClaim,
-) ecdsaabi.EcdsaInactivityClaim {
- inactiveMembersIndices := make([]*big.Int, len(claim.InactiveMembersIndices))
- for i, memberIndex := range claim.InactiveMembersIndices {
- inactiveMembersIndices[i] = big.NewInt(int64(memberIndex))
- }
-
- signingMembersIndices := make([]*big.Int, len(claim.SigningMembersIndices))
- for i, memberIndex := range claim.SigningMembersIndices {
- signingMembersIndices[i] = big.NewInt(int64(memberIndex))
- }
-
- return ecdsaabi.EcdsaInactivityClaim{
- WalletID: claim.WalletID,
- InactiveMembersIndices: inactiveMembersIndices,
- HeartbeatFailed: claim.HeartbeatFailed,
- Signatures: claim.Signatures,
- SigningMembersIndices: signingMembersIndices,
- }
-}
-
-func (tc *TbtcChain) SubmitInactivityClaim(
- claim *tbtc.InactivityClaim,
- nonce *big.Int,
- groupMembers []uint32,
-) error {
- _, err := tc.walletRegistry.NotifyOperatorInactivity(
- convertInactivityClaimToAbiType(claim),
- nonce,
- groupMembers,
- )
-
- return err
-}
-
-func (tc *TbtcChain) CalculateInactivityClaimHash(
- claim *inactivity.ClaimPreimage,
-) (inactivity.ClaimHash, error) {
- walletPublicKeyBytes := secp256k1.Marshal(claim.WalletPublicKey)
- // Crop the 04 prefix as the calculateInactivityClaimHash function expects
- // an unprefixed 64-byte public key,
- unprefixedGroupPublicKeyBytes := walletPublicKeyBytes[1:]
-
- // The type representing inactive member index should be `big.Int` as the
- // smart contract reading the calculated hash uses `uint256` for inactive
- // member indexes.
- inactiveMembersIndexes := make([]*big.Int, len(claim.InactiveMembersIndexes))
- for i, index := range claim.InactiveMembersIndexes {
- inactiveMembersIndexes[i] = big.NewInt(int64(index))
- }
-
- return calculateInactivityClaimHash(
- tc.chainID,
- claim.Nonce,
- unprefixedGroupPublicKeyBytes,
- inactiveMembersIndexes,
- claim.HeartbeatFailed,
- )
-}
-
-func calculateInactivityClaimHash(
- chainID *big.Int,
- nonce *big.Int,
- walletPublicKey []byte,
- inactiveMembersIndexes []*big.Int,
- heartbeatFailed bool,
-) (inactivity.ClaimHash, error) {
- publicKeySize := 64
-
- if len(walletPublicKey) != publicKeySize {
- return inactivity.ClaimHash{}, fmt.Errorf(
- "wrong wallet public key length",
- )
- }
-
- uint256Type, err := abi.NewType("uint256", "uint256", nil)
- if err != nil {
- return inactivity.ClaimHash{}, err
- }
- bytesType, err := abi.NewType("bytes", "bytes", nil)
- if err != nil {
- return inactivity.ClaimHash{}, err
- }
- uint256SliceType, err := abi.NewType("uint256[]", "uint256[]", nil)
- if err != nil {
- return inactivity.ClaimHash{}, err
- }
- boolType, err := abi.NewType("bool", "bool", nil)
- if err != nil {
- return inactivity.ClaimHash{}, err
- }
-
- bytes, err := abi.Arguments{
- {Type: uint256Type},
- {Type: uint256Type},
- {Type: bytesType},
- {Type: uint256SliceType},
- {Type: boolType},
- }.Pack(
- chainID,
- nonce,
- walletPublicKey,
- inactiveMembersIndexes,
- heartbeatFailed,
- )
- if err != nil {
- return inactivity.ClaimHash{}, err
- }
-
- return inactivity.ClaimHash(crypto.Keccak256Hash(bytes)), nil
-}
-
-func (tc *TbtcChain) GetInactivityClaimNonce(
- walletID [32]byte,
-) (*big.Int, error) {
- nonce, err := tc.walletRegistry.InactivityClaimNonce(walletID)
- if err != nil {
- return nil, fmt.Errorf(
- "failed to get inactivity claim nonce: [%w]",
- err,
- )
- }
-
- return nonce, nil
-}
-
-func (tc *TbtcChain) PastDepositRevealedEvents(
- filter *tbtc.DepositRevealedEventFilter,
-) ([]*tbtc.DepositRevealedEvent, error) {
- var startBlock uint64
- var endBlock *uint64
- var depositor []common.Address
- var walletPublicKeyHash [][20]byte
-
- if filter != nil {
- startBlock = filter.StartBlock
- endBlock = filter.EndBlock
-
- for _, d := range filter.Depositor {
- depositor = append(depositor, common.HexToAddress(d.String()))
- }
-
- walletPublicKeyHash = filter.WalletPublicKeyHash
- }
-
- events, err := tc.bridge.PastDepositRevealedEvents(
- startBlock,
- endBlock,
- depositor,
- walletPublicKeyHash,
- )
- if err != nil {
- return nil, err
- }
-
- convertedEvents := make([]*tbtc.DepositRevealedEvent, 0)
- for _, event := range events {
- var vault *chain.Address
- if event.Vault != [20]byte{} {
- v := chain.Address(event.Vault.Hex())
- vault = &v
- }
-
- convertedEvent := &tbtc.DepositRevealedEvent{
- // We can map the event.FundingTxHash field directly to the
- // bitcoin.Hash type. This is because event.FundingTxHash is
- // a [32]byte type representing a hash in the bitcoin.InternalByteOrder,
- // just as bitcoin.Hash assumes.
- FundingTxHash: event.FundingTxHash,
- FundingOutputIndex: event.FundingOutputIndex,
- Depositor: chain.Address(event.Depositor.Hex()),
- Amount: event.Amount,
- BlindingFactor: event.BlindingFactor,
- WalletPublicKeyHash: event.WalletPubKeyHash,
- RefundPublicKeyHash: event.RefundPubKeyHash,
- RefundLocktime: event.RefundLocktime,
- Vault: vault,
- BlockNumber: event.Raw.BlockNumber,
- }
-
- convertedEvents = append(convertedEvents, convertedEvent)
- }
-
- sort.SliceStable(
- convertedEvents,
- func(i, j int) bool {
- return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
- },
- )
-
- return convertedEvents, err
-}
-
-func (tc *TbtcChain) PastRedemptionRequestedEvents(
- filter *tbtc.RedemptionRequestedEventFilter,
-) ([]*tbtc.RedemptionRequestedEvent, error) {
- var startBlock uint64
- var endBlock *uint64
- var redeemers []common.Address
- var walletPublicKeyHash [][20]byte
-
- if filter != nil {
- startBlock = filter.StartBlock
- endBlock = filter.EndBlock
-
- for _, r := range filter.Redeemer {
- redeemers = append(redeemers, common.HexToAddress(r.String()))
- }
-
- walletPublicKeyHash = filter.WalletPublicKeyHash
- }
-
- events, err := tc.bridge.PastRedemptionRequestedEvents(
- startBlock,
- endBlock,
- walletPublicKeyHash,
- redeemers,
- )
- if err != nil {
- return nil, err
- }
-
- convertedEvents := make([]*tbtc.RedemptionRequestedEvent, 0)
- for _, event := range events {
- redeemerOutputScript, err := bitcoin.NewScriptFromVarLenData(
- event.RedeemerOutputScript,
- )
- if err != nil {
- return nil, err
- }
-
- convertedEvent := &tbtc.RedemptionRequestedEvent{
- WalletPublicKeyHash: event.WalletPubKeyHash,
- RedeemerOutputScript: redeemerOutputScript,
- Redeemer: chain.Address(event.Redeemer.Hex()),
- RequestedAmount: event.RequestedAmount,
- TreasuryFee: event.TreasuryFee,
- TxMaxFee: event.TreasuryFee,
- BlockNumber: event.Raw.BlockNumber,
- }
-
- convertedEvents = append(convertedEvents, convertedEvent)
- }
-
- sort.SliceStable(
- convertedEvents,
- func(i, j int) bool {
- return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
- },
- )
-
- return convertedEvents, err
-}
-
-func (tc *TbtcChain) GetDepositRequest(
- fundingTxHash bitcoin.Hash,
- fundingOutputIndex uint32,
-) (*tbtc.DepositChainRequest, bool, error) {
- depositKey := buildDepositKey(fundingTxHash, fundingOutputIndex)
- depositCacheKey := depositKey.Text(16)
-
- tc.sweptDepositsCache.Sweep()
- if cachedRequest, ok := tc.sweptDepositsCache.Get(depositCacheKey); ok {
- return cachedRequest, true, nil
- }
-
- chainRequest, err := tc.bridge.Deposits(depositKey)
- if err != nil {
- return nil, false, fmt.Errorf(
- "cannot get deposit request for key [0x%x]: [%v]",
- depositKey.Text(16),
- err,
- )
- }
-
- // Deposit not found.
- if chainRequest.RevealedAt == 0 {
- return nil, false, nil
- }
-
- var vault *chain.Address
- if chainRequest.Vault != [20]byte{} {
- v := chain.Address(chainRequest.Vault.Hex())
- vault = &v
- }
-
- var extraData *[32]byte
- if chainRequest.ExtraData != [32]byte{} {
- extraData = &chainRequest.ExtraData
- }
-
- request := &tbtc.DepositChainRequest{
- Depositor: chain.Address(chainRequest.Depositor.Hex()),
- Amount: chainRequest.Amount,
- RevealedAt: time.Unix(int64(chainRequest.RevealedAt), 0),
- Vault: vault,
- TreasuryFee: chainRequest.TreasuryFee,
- SweptAt: time.Unix(int64(chainRequest.SweptAt), 0),
- ExtraData: extraData,
- }
-
- // If the request was swept on-chain, there is a guarantee that no
- // further changes will occur regarding its parameters.
- // Such a request can be cached.
- if isSwept := request.SweptAt.Unix() != 0; isSwept {
- tc.sweptDepositsCache.Add(depositCacheKey, request)
- }
-
- return request, true, nil
-}
-
-func (tc *TbtcChain) PastNewWalletRegisteredEvents(
- filter *tbtc.NewWalletRegisteredEventFilter,
-) ([]*tbtc.NewWalletRegisteredEvent, error) {
- var startBlock uint64
- var endBlock *uint64
- var ecdsaWalletID [][32]byte
- var walletPublicKeyHash [][20]byte
-
- if filter != nil {
- startBlock = filter.StartBlock
- endBlock = filter.EndBlock
- ecdsaWalletID = filter.EcdsaWalletID
- walletPublicKeyHash = filter.WalletPublicKeyHash
- }
-
- events, err := tc.bridge.PastNewWalletRegisteredEvents(
- startBlock,
- endBlock,
- ecdsaWalletID,
- walletPublicKeyHash,
- )
- if err != nil {
- return nil, err
- }
-
- convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0)
- for _, event := range events {
- convertedEvent := &tbtc.NewWalletRegisteredEvent{
- EcdsaWalletID: event.EcdsaWalletID,
- WalletPublicKeyHash: event.WalletPubKeyHash,
- BlockNumber: event.Raw.BlockNumber,
- }
-
- convertedEvents = append(convertedEvents, convertedEvent)
- }
-
- sort.SliceStable(
- convertedEvents,
- func(i, j int) bool {
- return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
- },
- )
-
- return convertedEvents, err
-}
-
-func (tc *TbtcChain) CalculateWalletID(
- walletPublicKey *ecdsa.PublicKey,
-) ([32]byte, error) {
- return calculateWalletID(walletPublicKey)
-}
-
-func calculateWalletID(walletPublicKey *ecdsa.PublicKey) ([32]byte, error) {
- walletPublicKeyBytes, err := convertPubKeyToChainFormat(walletPublicKey)
- if err != nil {
- return [32]byte{}, fmt.Errorf(
- "error while converting wallet public key to chain format: [%v]",
- err,
- )
- }
-
- return crypto.Keccak256Hash(walletPublicKeyBytes[:]), nil
-}
-
-func (tc *TbtcChain) IsWalletRegistered(EcdsaWalletID [32]byte) (bool, error) {
- isWalletRegistered, err := tc.walletRegistry.IsWalletRegistered(
- EcdsaWalletID,
- )
- if err != nil {
- return false, fmt.Errorf(
- "cannot check if wallet with ECDSA ID [0x%x] is registered: [%v]",
- EcdsaWalletID,
- err,
- )
- }
-
- return isWalletRegistered, nil
-}
-
-func (tc *TbtcChain) GetWallet(
- walletPublicKeyHash [20]byte,
-) (*tbtc.WalletChainData, error) {
- wallet, err := tc.bridge.Wallets(walletPublicKeyHash)
- if err != nil {
- return nil, fmt.Errorf(
- "cannot get wallet for public key hash [0x%x]: [%v]",
- walletPublicKeyHash,
- err,
- )
- }
-
- // Wallet not found.
- if wallet.CreatedAt == 0 {
- return nil, fmt.Errorf(
- "no wallet for public key hash [0x%x]",
- wallet,
- )
- }
-
- walletState, err := parseWalletState(wallet.State)
- if err != nil {
- return nil, fmt.Errorf("cannot parse wallet state: [%v]", err)
- }
-
- return &tbtc.WalletChainData{
- EcdsaWalletID: wallet.EcdsaWalletID,
- MainUtxoHash: wallet.MainUtxoHash,
- PendingRedemptionsValue: wallet.PendingRedemptionsValue,
- CreatedAt: time.Unix(int64(wallet.CreatedAt), 0),
- MovingFundsRequestedAt: time.Unix(int64(wallet.MovingFundsRequestedAt), 0),
- ClosingStartedAt: time.Unix(int64(wallet.ClosingStartedAt), 0),
- PendingMovedFundsSweepRequestsCount: wallet.PendingMovedFundsSweepRequestsCount,
- State: walletState,
- MovingFundsTargetWalletsCommitmentHash: wallet.MovingFundsTargetWalletsCommitmentHash,
- }, nil
-}
-
-func (tc *TbtcChain) OnWalletClosed(
- handler func(event *tbtc.WalletClosedEvent),
-) subscription.EventSubscription {
- onEvent := func(
- walletID [32]byte,
- blockNumber uint64,
- ) {
- handler(&tbtc.WalletClosedEvent{
- WalletID: walletID,
- BlockNumber: blockNumber,
- })
- }
- return tc.walletRegistry.WalletClosedEvent(nil, nil).OnEvent(onEvent)
-}
-
-func (tc *TbtcChain) ComputeMainUtxoHash(
- mainUtxo *bitcoin.UnspentTransactionOutput,
-) [32]byte {
- return computeMainUtxoHash(mainUtxo)
-}
-
-func computeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte {
- outputIndexBytes := make([]byte, 4)
- binary.BigEndian.PutUint32(outputIndexBytes, mainUtxo.Outpoint.OutputIndex)
-
- valueBytes := make([]byte, 8)
- binary.BigEndian.PutUint64(valueBytes, uint64(mainUtxo.Value))
-
- mainUtxoHash := crypto.Keccak256Hash(
- append(
- append(
- mainUtxo.Outpoint.TransactionHash[:],
- outputIndexBytes...,
- ), valueBytes...,
- ),
- )
-
- return mainUtxoHash
-}
-
-func (tc *TbtcChain) ComputeMovingFundsCommitmentHash(
- targetWallets [][20]byte,
-) [32]byte {
- return computeMovingFundsCommitmentHash(targetWallets)
-}
-
-func computeMovingFundsCommitmentHash(targetWallets [][20]byte) [32]byte {
- packedWallets := []byte{}
-
- for _, wallet := range targetWallets {
- packedWallets = append(packedWallets, wallet[:]...)
- // Each wallet hash must be padded with 12 zero bytes following the
- // actual hash.
- packedWallets = append(packedWallets, make([]byte, 12)...)
- }
-
- return crypto.Keccak256Hash(packedWallets)
-}
-
-func (tc *TbtcChain) BuildDepositKey(
- fundingTxHash bitcoin.Hash,
- fundingOutputIndex uint32,
-) *big.Int {
- return buildDepositKey(fundingTxHash, fundingOutputIndex)
-}
-
-func (tc *TbtcChain) BuildRedemptionKey(
- walletPublicKeyHash [20]byte,
- redeemerOutputScript bitcoin.Script,
-) (*big.Int, error) {
- return buildRedemptionKey(walletPublicKeyHash, redeemerOutputScript)
-}
-
-func (tc *TbtcChain) GetDepositParameters() (tbtc.DepositParameters, error) {
- parameters, err := tc.bridge.DepositParameters()
- if err != nil {
- return tbtc.DepositParameters{}, err
- }
-
- return tbtc.DepositParameters{
- DustThreshold: parameters.DepositDustThreshold,
- TreasuryFeeDivisor: parameters.DepositTreasuryFeeDivisor,
- TxMaxFee: parameters.DepositTxMaxFee,
- RevealAheadPeriod: parameters.DepositRevealAheadPeriod,
- }, nil
-}
-
-func (tc *TbtcChain) GetPendingRedemptionRequest(
- walletPublicKeyHash [20]byte,
- redeemerOutputScript bitcoin.Script,
-) (*tbtc.RedemptionRequest, bool, error) {
- redemptionKey, err := buildRedemptionKey(walletPublicKeyHash, redeemerOutputScript)
- if err != nil {
- return nil, false, fmt.Errorf("cannot build redemption key: [%v]", err)
- }
-
- redemptionRequest, err := tc.bridge.PendingRedemptions(redemptionKey)
- if err != nil {
- return nil, false, fmt.Errorf(
- "cannot get pending redemption request for key [0x%x]: [%v]",
- redemptionKey.Text(16),
- err,
- )
- }
-
- // Redemption not found.
- if redemptionRequest.RequestedAt == 0 {
- return nil, false, nil
- }
-
- return &tbtc.RedemptionRequest{
- Redeemer: chain.Address(redemptionRequest.Redeemer.Hex()),
- RedeemerOutputScript: redeemerOutputScript,
- RequestedAmount: redemptionRequest.RequestedAmount,
- TreasuryFee: redemptionRequest.TreasuryFee,
- TxMaxFee: redemptionRequest.TxMaxFee,
- RequestedAt: time.Unix(int64(redemptionRequest.RequestedAt), 0),
- }, true, nil
-}
-
-func (tc *TbtcChain) SubmitRedemptionProofWithReimbursement(
- transaction *bitcoin.Transaction,
- proof *bitcoin.SpvProof,
- mainUTXO bitcoin.UnspentTransactionOutput,
- walletPublicKeyHash [20]byte,
-) error {
- bitcoinTxInfo := tbtcabi.BitcoinTxInfo3{
- Version: transaction.SerializeVersion(),
- InputVector: transaction.SerializeInputs(),
- OutputVector: transaction.SerializeOutputs(),
- Locktime: transaction.SerializeLocktime(),
- }
- redemptionProof := tbtcabi.BitcoinTxProof2{
- MerkleProof: proof.MerkleProof,
- TxIndexInBlock: big.NewInt(int64(proof.TxIndexInBlock)),
- BitcoinHeaders: proof.BitcoinHeaders,
- CoinbasePreimage: proof.CoinbasePreimage,
- CoinbaseProof: proof.CoinbaseProof,
- }
- utxo := tbtcabi.BitcoinTxUTXO2{
- TxHash: mainUTXO.Outpoint.TransactionHash,
- TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
- TxOutputValue: uint64(mainUTXO.Value),
- }
-
- gasEstimate, err := tc.maintainerProxy.SubmitRedemptionProofGasEstimate(
- bitcoinTxInfo,
- redemptionProof,
- utxo,
- walletPublicKeyHash,
- )
- if err != nil {
- return err
- }
-
- // The original estimate for this contract call is too low and the call
- // fails on reimbursing the submitter. Example:
- // 0xe27a92883e0e64da8a3a54a15a260ea2f4d3d48470129ac5c09bfe9637d7e114
- // Here we add a 20% margin to overcome the gas problems.
- gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
-
- _, err = tc.maintainerProxy.SubmitRedemptionProof(
- bitcoinTxInfo,
- redemptionProof,
- utxo,
- walletPublicKeyHash,
- ethutil.TransactionOptions{
- GasLimit: uint64(gasEstimateWithMargin),
- },
- )
-
- return err
-}
-
-func buildRedemptionKey(
- walletPublicKeyHash [20]byte,
- redeemerOutputScript bitcoin.Script,
-) (*big.Int, error) {
- // The Bridge contract builds the redemption key using the length-prefixed
- // redeemer output script.
- prefixedRedeemerOutputScript, err := redeemerOutputScript.ToVarLenData()
- if err != nil {
- return nil, fmt.Errorf("cannot build prefixed redeemer output script: [%v]", err)
- }
-
- redeemerOutputScriptHash := crypto.Keccak256Hash(prefixedRedeemerOutputScript)
-
- redemptionKey := crypto.Keccak256Hash(
- append(redeemerOutputScriptHash[:], walletPublicKeyHash[:]...),
- )
-
- return redemptionKey.Big(), nil
-}
-
func (tc *TbtcChain) TxProofDifficultyFactor() (*big.Int, error) {
return tc.bridge.TxProofDifficultyFactor()
}
-
-func (tc *TbtcChain) SubmitDepositSweepProofWithReimbursement(
- transaction *bitcoin.Transaction,
- proof *bitcoin.SpvProof,
- mainUTXO bitcoin.UnspentTransactionOutput,
- vault common.Address,
-) error {
- bitcoinTxInfo := tbtcabi.BitcoinTxInfo3{
- Version: transaction.SerializeVersion(),
- InputVector: transaction.SerializeInputs(),
- OutputVector: transaction.SerializeOutputs(),
- Locktime: transaction.SerializeLocktime(),
- }
- sweepProof := tbtcabi.BitcoinTxProof2{
- MerkleProof: proof.MerkleProof,
- TxIndexInBlock: big.NewInt(int64(proof.TxIndexInBlock)),
- BitcoinHeaders: proof.BitcoinHeaders,
- CoinbasePreimage: proof.CoinbasePreimage,
- CoinbaseProof: proof.CoinbaseProof,
- }
- utxo := tbtcabi.BitcoinTxUTXO2{
- TxHash: mainUTXO.Outpoint.TransactionHash,
- TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
- TxOutputValue: uint64(mainUTXO.Value),
- }
-
- gasEstimate, err := tc.maintainerProxy.SubmitDepositSweepProofGasEstimate(
- bitcoinTxInfo,
- sweepProof,
- utxo,
- vault,
- )
- if err != nil {
- return err
- }
-
- // The original estimate for this contract call is too low and the call
- // fails on reimbursing the submitter. Example:
- // 0xe27a92883e0e64da8a3a54a15a260ea2f4d3d48470129ac5c09bfe9637d7e114
- // Here we add a 20% margin to overcome the gas problems.
- gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
-
- _, err = tc.maintainerProxy.SubmitDepositSweepProof(
- bitcoinTxInfo,
- sweepProof,
- utxo,
- vault,
- ethutil.TransactionOptions{
- GasLimit: uint64(gasEstimateWithMargin),
- },
- )
-
- return err
-}
-
-func (tc *TbtcChain) GetRedemptionParameters() (tbtc.RedemptionParameters, error) {
- parameters, err := tc.bridge.RedemptionParameters()
- if err != nil {
- return tbtc.RedemptionParameters{}, err
- }
-
- return tbtc.RedemptionParameters{
- DustThreshold: parameters.RedemptionDustThreshold,
- TreasuryFeeDivisor: parameters.RedemptionTreasuryFeeDivisor,
- TxMaxFee: parameters.RedemptionTxMaxFee,
- TxMaxTotalFee: parameters.RedemptionTxMaxTotalFee,
- Timeout: parameters.RedemptionTimeout,
- TimeoutSlashingAmount: parameters.RedemptionTimeoutSlashingAmount,
- TimeoutNotifierRewardMultiplier: parameters.RedemptionTimeoutNotifierRewardMultiplier,
- }, nil
-}
-
-func (tc *TbtcChain) GetWalletParameters() (tbtc.WalletParameters, error) {
- parameters, err := tc.bridge.WalletParameters()
- if err != nil {
- return tbtc.WalletParameters{}, err
- }
-
- return tbtc.WalletParameters{
- CreationPeriod: parameters.WalletCreationPeriod,
- CreationMinBtcBalance: parameters.WalletCreationMinBtcBalance,
- CreationMaxBtcBalance: parameters.WalletCreationMaxBtcBalance,
- ClosureMinBtcBalance: parameters.WalletClosureMinBtcBalance,
- MaxAge: parameters.WalletMaxAge,
- MaxBtcTransfer: parameters.WalletMaxBtcTransfer,
- ClosingPeriod: parameters.WalletClosingPeriod,
- }, nil
-}
-
-func (tc *TbtcChain) GetLiveWalletsCount() (uint32, error) {
- return tc.bridge.LiveWalletsCount()
-}
-
-func (tc *TbtcChain) PastMovingFundsCommitmentSubmittedEvents(
- filter *tbtc.MovingFundsCommitmentSubmittedEventFilter,
-) ([]*tbtc.MovingFundsCommitmentSubmittedEvent, error) {
- var startBlock uint64
- var endBlock *uint64
- var walletPublicKeyHash [][20]byte
-
- if filter != nil {
- startBlock = filter.StartBlock
- endBlock = filter.EndBlock
- walletPublicKeyHash = filter.WalletPublicKeyHash
- }
-
- events, err := tc.bridge.PastMovingFundsCommitmentSubmittedEvents(
- startBlock,
- endBlock,
- walletPublicKeyHash,
- )
- if err != nil {
- return nil, err
- }
-
- convertedEvents := make([]*tbtc.MovingFundsCommitmentSubmittedEvent, 0)
- for _, event := range events {
- convertedEvent := &tbtc.MovingFundsCommitmentSubmittedEvent{
- WalletPublicKeyHash: event.WalletPubKeyHash,
- TargetWallets: event.TargetWallets,
- Submitter: chain.Address(event.Submitter.Hex()),
- BlockNumber: event.Raw.BlockNumber,
- }
-
- convertedEvents = append(convertedEvents, convertedEvent)
- }
-
- sort.SliceStable(
- convertedEvents,
- func(i, j int) bool {
- return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
- },
- )
-
- return convertedEvents, err
-}
-
-func (tc *TbtcChain) PastMovingFundsCompletedEvents(
- filter *tbtc.MovingFundsCompletedEventFilter,
-) ([]*tbtc.MovingFundsCompletedEvent, error) {
- var startBlock uint64
- var endBlock *uint64
- var walletPublicKeyHash [][20]byte
-
- if filter != nil {
- startBlock = filter.StartBlock
- endBlock = filter.EndBlock
- walletPublicKeyHash = filter.WalletPublicKeyHash
- }
-
- events, err := tc.bridge.PastMovingFundsCompletedEvents(
- startBlock,
- endBlock,
- walletPublicKeyHash,
- )
- if err != nil {
- return nil, err
- }
-
- convertedEvents := make([]*tbtc.MovingFundsCompletedEvent, 0)
- for _, event := range events {
- convertedEvent := &tbtc.MovingFundsCompletedEvent{
- WalletPublicKeyHash: event.WalletPubKeyHash,
- MovingFundsTxHash: event.MovingFundsTxHash,
- BlockNumber: event.Raw.BlockNumber,
- }
-
- convertedEvents = append(convertedEvents, convertedEvent)
- }
-
- sort.SliceStable(
- convertedEvents,
- func(i, j int) bool {
- return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
- },
- )
-
- return convertedEvents, err
-}
-
-func buildDepositKey(
- fundingTxHash bitcoin.Hash,
- fundingOutputIndex uint32,
-) *big.Int {
- fundingOutputIndexBytes := make([]byte, 4)
- binary.BigEndian.PutUint32(fundingOutputIndexBytes, fundingOutputIndex)
-
- depositKey := crypto.Keccak256Hash(
- append(fundingTxHash[:], fundingOutputIndexBytes...),
- )
-
- return depositKey.Big()
-}
-
-func convertDepositSweepProposalToAbiType(
- walletPublicKeyHash [20]byte,
- proposal *tbtc.DepositSweepProposal,
-) tbtcabi.WalletProposalValidatorDepositSweepProposal {
- depositsKeys := make(
- []tbtcabi.WalletProposalValidatorDepositKey,
- len(proposal.DepositsKeys),
- )
-
- for i, depositKey := range proposal.DepositsKeys {
- // We can map the depositKey.FundingTxHash field directly to the
- // [32]byte type. This is because depositKey.FundingTxHash is
- // a bitcoin.Hash type representing a hash in the
- // bitcoin.InternalByteOrder, just as the on-chain contract assumes.
- depositsKeys[i] = tbtcabi.WalletProposalValidatorDepositKey{
- FundingTxHash: depositKey.FundingTxHash,
- FundingOutputIndex: depositKey.FundingOutputIndex,
- }
- }
-
- return tbtcabi.WalletProposalValidatorDepositSweepProposal{
- WalletPubKeyHash: walletPublicKeyHash,
- DepositsKeys: depositsKeys,
- SweepTxFee: proposal.SweepTxFee,
- DepositsRevealBlocks: proposal.DepositsRevealBlocks,
- }
-}
-
-func parseWalletState(value uint8) (tbtc.WalletState, error) {
- switch value {
- case 0:
- return tbtc.StateUnknown, nil
- case 1:
- return tbtc.StateLive, nil
- case 2:
- return tbtc.StateMovingFunds, nil
- case 3:
- return tbtc.StateClosing, nil
- case 4:
- return tbtc.StateClosed, nil
- case 5:
- return tbtc.StateTerminated, nil
- default:
- return 0, fmt.Errorf("unexpected wallet state value: [%v]", value)
- }
-}
-
-func (tc *TbtcChain) ValidateDepositSweepProposal(
- walletPublicKeyHash [20]byte,
- proposal *tbtc.DepositSweepProposal,
- depositsExtraInfo []struct {
- *tbtc.Deposit
- FundingTx *bitcoin.Transaction
- },
-) error {
- dei := make([]tbtcabi.WalletProposalValidatorDepositExtraInfo, len(depositsExtraInfo))
- for i, depositExtraInfo := range depositsExtraInfo {
- fundingTx := tbtcabi.BitcoinTxInfo2{
- Version: depositExtraInfo.FundingTx.SerializeVersion(),
- InputVector: depositExtraInfo.FundingTx.SerializeInputs(),
- OutputVector: depositExtraInfo.FundingTx.SerializeOutputs(),
- Locktime: depositExtraInfo.FundingTx.SerializeLocktime(),
- }
-
- dei[i] = tbtcabi.WalletProposalValidatorDepositExtraInfo{
- FundingTx: fundingTx,
- BlindingFactor: depositExtraInfo.Deposit.BlindingFactor,
- WalletPubKeyHash: depositExtraInfo.Deposit.WalletPublicKeyHash,
- RefundPubKeyHash: depositExtraInfo.Deposit.RefundPublicKeyHash,
- RefundLocktime: depositExtraInfo.Deposit.RefundLocktime,
- }
- }
-
- valid, err := tc.walletProposalValidator.ValidateDepositSweepProposal(
- convertDepositSweepProposalToAbiType(walletPublicKeyHash, proposal),
- dei,
- )
- if err != nil {
- return fmt.Errorf("validation failed: [%v]", err)
- }
-
- // Should never happen because `validateDepositSweepProposal` returns true
- // or reverts (returns an error) but do the check just in case.
- if !valid {
- return fmt.Errorf("unexpected validation result")
- }
-
- return nil
-}
-
-func (tc *TbtcChain) GetDepositSweepMaxSize() (uint16, error) {
- return tc.walletProposalValidator.DEPOSITSWEEPMAXSIZE()
-}
-
-func (tc *TbtcChain) SubmitMovingFundsCommitment(
- walletPublicKeyHash [20]byte,
- walletMainUTXO bitcoin.UnspentTransactionOutput,
- walletMembersIDs []uint32,
- walletMemberIndex uint32,
- targetWallets [][20]byte,
-) error {
- mainUtxo := tbtcabi.BitcoinTxUTXO{
- TxHash: walletMainUTXO.Outpoint.TransactionHash,
- TxOutputIndex: walletMainUTXO.Outpoint.OutputIndex,
- TxOutputValue: uint64(walletMainUTXO.Value),
- }
- _, err := tc.bridge.SubmitMovingFundsCommitment(
- walletPublicKeyHash,
- mainUtxo,
- walletMembersIDs,
- big.NewInt(int64(walletMemberIndex)),
- targetWallets,
- )
- return err
-}
-
-func (tc *TbtcChain) SubmitMovingFundsProofWithReimbursement(
- transaction *bitcoin.Transaction,
- proof *bitcoin.SpvProof,
- mainUTXO bitcoin.UnspentTransactionOutput,
- walletPublicKeyHash [20]byte,
-) error {
- bitcoinTxInfo := tbtcabi.BitcoinTxInfo3{
- Version: transaction.SerializeVersion(),
- InputVector: transaction.SerializeInputs(),
- OutputVector: transaction.SerializeOutputs(),
- Locktime: transaction.SerializeLocktime(),
- }
- movingFundsProof := tbtcabi.BitcoinTxProof2{
- MerkleProof: proof.MerkleProof,
- TxIndexInBlock: big.NewInt(int64(proof.TxIndexInBlock)),
- BitcoinHeaders: proof.BitcoinHeaders,
- CoinbasePreimage: proof.CoinbasePreimage,
- CoinbaseProof: proof.CoinbaseProof,
- }
- utxo := tbtcabi.BitcoinTxUTXO2{
- TxHash: mainUTXO.Outpoint.TransactionHash,
- TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
- TxOutputValue: uint64(mainUTXO.Value),
- }
-
- gasEstimate, err := tc.maintainerProxy.SubmitMovingFundsProofGasEstimate(
- bitcoinTxInfo,
- movingFundsProof,
- utxo,
- walletPublicKeyHash,
- )
- if err != nil {
- return err
- }
-
- // The original estimate for this contract call is too low and the call
- // fails on reimbursing the submitter. Example:
- // 0xe27a92883e0e64da8a3a54a15a260ea2f4d3d48470129ac5c09bfe9637d7e114
- // Here we add a 20% margin to overcome the gas problems.
- gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
-
- _, err = tc.maintainerProxy.SubmitMovingFundsProof(
- bitcoinTxInfo,
- movingFundsProof,
- utxo,
- walletPublicKeyHash,
- ethutil.TransactionOptions{
- GasLimit: uint64(gasEstimateWithMargin),
- },
- )
-
- return err
-}
-
-func (tc *TbtcChain) SubmitMovedFundsSweepProofWithReimbursement(
- transaction *bitcoin.Transaction,
- proof *bitcoin.SpvProof,
- mainUTXO bitcoin.UnspentTransactionOutput,
-) error {
- bitcoinTxInfo := tbtcabi.BitcoinTxInfo3{
- Version: transaction.SerializeVersion(),
- InputVector: transaction.SerializeInputs(),
- OutputVector: transaction.SerializeOutputs(),
- Locktime: transaction.SerializeLocktime(),
- }
- movedFundsSweepProof := tbtcabi.BitcoinTxProof2{
- MerkleProof: proof.MerkleProof,
- TxIndexInBlock: big.NewInt(int64(proof.TxIndexInBlock)),
- BitcoinHeaders: proof.BitcoinHeaders,
- CoinbasePreimage: proof.CoinbasePreimage,
- CoinbaseProof: proof.CoinbaseProof,
- }
- utxo := tbtcabi.BitcoinTxUTXO2{
- TxHash: mainUTXO.Outpoint.TransactionHash,
- TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
- TxOutputValue: uint64(mainUTXO.Value),
- }
-
- gasEstimate, err := tc.maintainerProxy.SubmitMovedFundsSweepProofGasEstimate(
- bitcoinTxInfo,
- movedFundsSweepProof,
- utxo,
- )
- if err != nil {
- return err
- }
-
- // The original estimate for this contract call is too low and the call
- // fails on reimbursing the submitter. Example:
- // 0xe27a92883e0e64da8a3a54a15a260ea2f4d3d48470129ac5c09bfe9637d7e114
- // Here we add a 20% margin to overcome the gas problems.
- gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
-
- _, err = tc.maintainerProxy.SubmitMovedFundsSweepProof(
- bitcoinTxInfo,
- movedFundsSweepProof,
- utxo,
- ethutil.TransactionOptions{
- GasLimit: uint64(gasEstimateWithMargin),
- },
- )
-
- return err
-}
-
-func (tc *TbtcChain) ValidateMovedFundsSweepProposal(
- walletPublicKeyHash [20]byte,
- proposal *tbtc.MovedFundsSweepProposal,
-) error {
- abiProposal := tbtcabi.WalletProposalValidatorMovedFundsSweepProposal{
- WalletPubKeyHash: walletPublicKeyHash,
- MovingFundsTxHash: proposal.MovingFundsTxHash,
- MovingFundsTxOutputIndex: proposal.MovingFundsTxOutputIndex,
- MovedFundsSweepTxFee: proposal.SweepTxFee,
- }
-
- valid, err := tc.walletProposalValidator.ValidateMovedFundsSweepProposal(
- abiProposal,
- )
- if err != nil {
- return fmt.Errorf("validation failed: [%v]", err)
- }
-
- // Should never happen because `validateMovedFundsSweepProposal` returns
- // true or reverts (returns an error) but do the check just in case.
- if !valid {
- return fmt.Errorf("unexpected validation result")
- }
-
- return nil
-}
-
-func (tc *TbtcChain) ValidateRedemptionProposal(
- walletPublicKeyHash [20]byte,
- proposal *tbtc.RedemptionProposal,
-) error {
- abiProposal, err := convertRedemptionProposalToAbiType(
- walletPublicKeyHash,
- proposal,
- )
- if err != nil {
- return fmt.Errorf("cannot convert proposal to abi type: [%v]", err)
- }
-
- valid, err := tc.walletProposalValidator.ValidateRedemptionProposal(
- abiProposal,
- )
- if err != nil {
- return fmt.Errorf("validation failed: [%v]", err)
- }
-
- // Should never happen because `validateRedemptionProposal` returns true
- // or reverts (returns an error) but do the check just in case.
- if !valid {
- return fmt.Errorf("unexpected validation result")
- }
-
- return nil
-}
-
-func convertRedemptionProposalToAbiType(
- walletPublicKeyHash [20]byte,
- proposal *tbtc.RedemptionProposal,
-) (tbtcabi.WalletProposalValidatorRedemptionProposal, error) {
- redeemersOutputScripts := make(
- [][]byte,
- len(proposal.RedeemersOutputScripts),
- )
-
- for i, script := range proposal.RedeemersOutputScripts {
- // The on-chain script representation must be prepended with the script's
- // byte-length while bitcoin.Script is not. We need to add the
- // length prefix.
- prefixedScript, err := script.ToVarLenData()
- if err != nil {
- return tbtcabi.WalletProposalValidatorRedemptionProposal{}, fmt.Errorf(
- "cannot convert redeemer output script: [%v]",
- err,
- )
- }
-
- redeemersOutputScripts[i] = prefixedScript
- }
-
- return tbtcabi.WalletProposalValidatorRedemptionProposal{
- WalletPubKeyHash: walletPublicKeyHash,
- RedeemersOutputScripts: redeemersOutputScripts,
- RedemptionTxFee: proposal.RedemptionTxFee,
- }, nil
-}
-
-func (tc *TbtcChain) GetRedemptionMaxSize() (uint16, error) {
- return tc.walletProposalValidator.REDEMPTIONMAXSIZE()
-}
-
-func (tc *TbtcChain) GetRedemptionRequestMinAge() (uint32, error) {
- return tc.walletProposalValidator.REDEMPTIONREQUESTMINAGE()
-}
-
-func (tc *TbtcChain) ValidateHeartbeatProposal(
- walletPublicKeyHash [20]byte,
- proposal *tbtc.HeartbeatProposal,
-) error {
- valid, err := tc.walletProposalValidator.ValidateHeartbeatProposal(
- tbtcabi.WalletProposalValidatorHeartbeatProposal{
- WalletPubKeyHash: walletPublicKeyHash,
- Message: proposal.Message[:],
- },
- )
- if err != nil {
- return fmt.Errorf("validation failed: [%v]", err)
- }
-
- // Should never happen because `validateHeartbeatProposal` returns true
- // or reverts (returns an error) but do the check just in case.
- if !valid {
- return fmt.Errorf("unexpected validation result")
- }
-
- return nil
-}
-
-func (tc *TbtcChain) GetMovingFundsParameters() (tbtc.MovingFundsParameters, error) {
- parameters, err := tc.bridge.MovingFundsParameters()
- if err != nil {
- return tbtc.MovingFundsParameters{}, err
- }
-
- return tbtc.MovingFundsParameters{
- TxMaxTotalFee: parameters.MovingFundsTxMaxTotalFee,
- DustThreshold: parameters.MovingFundsDustThreshold,
- TimeoutResetDelay: parameters.MovingFundsTimeoutResetDelay,
- Timeout: parameters.MovingFundsTimeout,
- TimeoutSlashingAmount: parameters.MovingFundsTimeoutSlashingAmount,
- TimeoutNotifierRewardMultiplier: parameters.MovingFundsTimeoutNotifierRewardMultiplier,
- CommitmentGasOffset: parameters.MovingFundsCommitmentGasOffset,
- SweepTxMaxTotalFee: parameters.MovedFundsSweepTxMaxTotalFee,
- SweepTimeout: parameters.MovedFundsSweepTimeout,
- SweepTimeoutSlashingAmount: parameters.MovedFundsSweepTimeoutSlashingAmount,
- SweepTimeoutNotifierRewardMultiplier: parameters.MovedFundsSweepTimeoutNotifierRewardMultiplier,
- }, nil
-}
-
-func (tc *TbtcChain) GetMovedFundsSweepRequest(
- movingFundsTxHash bitcoin.Hash,
- movingFundsTxOutpointIndex uint32,
-) (*tbtc.MovedFundsSweepRequest, bool, error) {
- movedFundsKey := buildMovedFundsKey(
- movingFundsTxHash,
- movingFundsTxOutpointIndex,
- )
-
- movedFundsSweepRequest, err := tc.bridge.MovedFundsSweepRequests(
- movedFundsKey,
- )
- if err != nil {
- return nil, false, fmt.Errorf(
- "cannot get moved funds sweep request for key [0x%x]: [%v]",
- movedFundsKey.Text(16),
- err,
- )
- }
-
- // Moved funds sweep request not found.
- if movedFundsSweepRequest.CreatedAt == 0 {
- return nil, false, nil
- }
-
- state, err := parseMovedFundsSweepRequestState(movedFundsSweepRequest.State)
- if err != nil {
- return nil, false, fmt.Errorf(
- "cannot parse state for moved funds sweep request [0x%x]: [%v]",
- movedFundsKey.Text(16),
- err,
- )
- }
-
- return &tbtc.MovedFundsSweepRequest{
- WalletPublicKeyHash: movedFundsSweepRequest.WalletPubKeyHash,
- Value: movedFundsSweepRequest.Value,
- CreatedAt: time.Unix(int64(movedFundsSweepRequest.CreatedAt), 0),
- State: state,
- }, true, nil
-}
-
-func parseMovedFundsSweepRequestState(value uint8) (
- tbtc.MovedFundsSweepRequestState,
- error,
-) {
- switch value {
- case 0:
- return tbtc.MovedFundsStateUnknown, nil
- case 1:
- return tbtc.MovedFundsStatePending, nil
- case 2:
- return tbtc.MovedFundsStateProcessed, nil
- case 3:
- return tbtc.MovedFundsStateTimedOut, nil
- default:
- return 0, fmt.Errorf(
- "unexpected moved funds sweep request state value: [%v]",
- value,
- )
- }
-}
-
-func buildMovedFundsKey(
- movingFundsTxHash bitcoin.Hash,
- movingFundsTxOutpointIndex uint32,
-) *big.Int {
- indexBytes := make([]byte, 4)
- binary.BigEndian.PutUint32(indexBytes, movingFundsTxOutpointIndex)
-
- movedFundsKey := crypto.Keccak256Hash(
- append(movingFundsTxHash[:], indexBytes...),
- )
-
- return movedFundsKey.Big()
-}
-
-func (tc *TbtcChain) ValidateMovingFundsProposal(
- walletPublicKeyHash [20]byte,
- mainUTXO *bitcoin.UnspentTransactionOutput,
- proposal *tbtc.MovingFundsProposal,
-) error {
- abiProposal := tbtcabi.WalletProposalValidatorMovingFundsProposal{
- WalletPubKeyHash: walletPublicKeyHash,
- TargetWallets: proposal.TargetWallets,
- MovingFundsTxFee: proposal.MovingFundsTxFee,
- }
- abiMainUTXO := tbtcabi.BitcoinTxUTXO3{
- TxHash: mainUTXO.Outpoint.TransactionHash,
- TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
- TxOutputValue: uint64(mainUTXO.Value),
- }
-
- valid, err := tc.walletProposalValidator.ValidateMovingFundsProposal(
- abiProposal,
- abiMainUTXO,
- )
- if err != nil {
- return fmt.Errorf("validation failed: [%v]", err)
- }
-
- // Should never happen because `validateMovingFundsProposal` returns true
- // or reverts (returns an error) but do the check just in case.
- if !valid {
- return fmt.Errorf("unexpected validation result")
- }
-
- return nil
-}
-
-func (tc *TbtcChain) GetRedemptionDelay(
- walletPublicKeyHash [20]byte,
- redeemerOutputScript bitcoin.Script,
-) (time.Duration, error) {
- if tc.redemptionWatchtower == nil {
- return 0, nil
- }
-
- redemptionKey, err := tc.BuildRedemptionKey(walletPublicKeyHash, redeemerOutputScript)
- if err != nil {
- return 0, fmt.Errorf("cannot build redemption key: [%v]", err)
- }
-
- delay, err := tc.redemptionWatchtower.GetRedemptionDelay(redemptionKey)
- if err != nil {
- return 0, fmt.Errorf("cannot get redemption delay: [%v]", err)
- }
-
- return time.Duration(delay) * time.Second, nil
-}
-
-func (tc *TbtcChain) GetDepositMinAge() (uint32, error) {
- return tc.walletProposalValidator.DEPOSITMINAGE()
-}
diff --git a/pkg/chain/ethereum/tbtc_deposit.go b/pkg/chain/ethereum/tbtc_deposit.go
new file mode 100644
index 0000000000..de9e784167
--- /dev/null
+++ b/pkg/chain/ethereum/tbtc_deposit.go
@@ -0,0 +1,310 @@
+package ethereum
+
+import (
+ "encoding/binary"
+ "fmt"
+ "math/big"
+ "sort"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil"
+ "github.com/keep-network/keep-core/pkg/bitcoin"
+
+ "github.com/keep-network/keep-core/pkg/chain"
+ tbtcabi "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi"
+ "github.com/keep-network/keep-core/pkg/tbtc"
+)
+
+func (tc *TbtcChain) PastDepositRevealedEvents(
+ filter *tbtc.DepositRevealedEventFilter,
+) ([]*tbtc.DepositRevealedEvent, error) {
+ var startBlock uint64
+ var endBlock *uint64
+ var depositor []common.Address
+ var walletPublicKeyHash [][20]byte
+
+ if filter != nil {
+ startBlock = filter.StartBlock
+ endBlock = filter.EndBlock
+
+ for _, d := range filter.Depositor {
+ depositor = append(depositor, common.HexToAddress(d.String()))
+ }
+
+ walletPublicKeyHash = filter.WalletPublicKeyHash
+ }
+
+ events, err := tc.bridge.PastDepositRevealedEvents(
+ startBlock,
+ endBlock,
+ depositor,
+ walletPublicKeyHash,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ convertedEvents := make([]*tbtc.DepositRevealedEvent, 0)
+ for _, event := range events {
+ var vault *chain.Address
+ if event.Vault != [20]byte{} {
+ v := chain.Address(event.Vault.Hex())
+ vault = &v
+ }
+
+ convertedEvent := &tbtc.DepositRevealedEvent{
+ // We can map the event.FundingTxHash field directly to the
+ // bitcoin.Hash type. This is because event.FundingTxHash is
+ // a [32]byte type representing a hash in the bitcoin.InternalByteOrder,
+ // just as bitcoin.Hash assumes.
+ FundingTxHash: event.FundingTxHash,
+ FundingOutputIndex: event.FundingOutputIndex,
+ Depositor: chain.Address(event.Depositor.Hex()),
+ Amount: event.Amount,
+ BlindingFactor: event.BlindingFactor,
+ WalletPublicKeyHash: event.WalletPubKeyHash,
+ RefundPublicKeyHash: event.RefundPubKeyHash,
+ RefundLocktime: event.RefundLocktime,
+ Vault: vault,
+ BlockNumber: event.Raw.BlockNumber,
+ }
+
+ convertedEvents = append(convertedEvents, convertedEvent)
+ }
+
+ sort.SliceStable(
+ convertedEvents,
+ func(i, j int) bool {
+ return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
+ },
+ )
+
+ return convertedEvents, err
+}
+
+func (tc *TbtcChain) GetDepositRequest(
+ fundingTxHash bitcoin.Hash,
+ fundingOutputIndex uint32,
+) (*tbtc.DepositChainRequest, bool, error) {
+ depositKey := buildDepositKey(fundingTxHash, fundingOutputIndex)
+ depositCacheKey := depositKey.Text(16)
+
+ tc.sweptDepositsCache.Sweep()
+ if cachedRequest, ok := tc.sweptDepositsCache.Get(depositCacheKey); ok {
+ return cachedRequest, true, nil
+ }
+
+ chainRequest, err := tc.bridge.Deposits(depositKey)
+ if err != nil {
+ return nil, false, fmt.Errorf(
+ "cannot get deposit request for key [0x%x]: [%v]",
+ depositKey.Text(16),
+ err,
+ )
+ }
+
+ // Deposit not found.
+ if chainRequest.RevealedAt == 0 {
+ return nil, false, nil
+ }
+
+ var vault *chain.Address
+ if chainRequest.Vault != [20]byte{} {
+ v := chain.Address(chainRequest.Vault.Hex())
+ vault = &v
+ }
+
+ var extraData *[32]byte
+ if chainRequest.ExtraData != [32]byte{} {
+ extraData = &chainRequest.ExtraData
+ }
+
+ request := &tbtc.DepositChainRequest{
+ Depositor: chain.Address(chainRequest.Depositor.Hex()),
+ Amount: chainRequest.Amount,
+ RevealedAt: time.Unix(int64(chainRequest.RevealedAt), 0),
+ Vault: vault,
+ TreasuryFee: chainRequest.TreasuryFee,
+ SweptAt: time.Unix(int64(chainRequest.SweptAt), 0),
+ ExtraData: extraData,
+ }
+
+ // If the request was swept on-chain, there is a guarantee that no
+ // further changes will occur regarding its parameters.
+ // Such a request can be cached.
+ if isSwept := request.SweptAt.Unix() != 0; isSwept {
+ tc.sweptDepositsCache.Add(depositCacheKey, request)
+ }
+
+ return request, true, nil
+}
+
+func (tc *TbtcChain) BuildDepositKey(
+ fundingTxHash bitcoin.Hash,
+ fundingOutputIndex uint32,
+) *big.Int {
+ return buildDepositKey(fundingTxHash, fundingOutputIndex)
+}
+
+func (tc *TbtcChain) GetDepositParameters() (tbtc.DepositParameters, error) {
+ parameters, err := tc.bridge.DepositParameters()
+ if err != nil {
+ return tbtc.DepositParameters{}, err
+ }
+
+ return tbtc.DepositParameters{
+ DustThreshold: parameters.DepositDustThreshold,
+ TreasuryFeeDivisor: parameters.DepositTreasuryFeeDivisor,
+ TxMaxFee: parameters.DepositTxMaxFee,
+ RevealAheadPeriod: parameters.DepositRevealAheadPeriod,
+ }, nil
+}
+
+func (tc *TbtcChain) SubmitDepositSweepProofWithReimbursement(
+ transaction *bitcoin.Transaction,
+ proof *bitcoin.SpvProof,
+ mainUTXO bitcoin.UnspentTransactionOutput,
+ vault common.Address,
+) error {
+ bitcoinTxInfo := tbtcabi.BitcoinTxInfo3{
+ Version: transaction.SerializeVersion(),
+ InputVector: transaction.SerializeInputs(),
+ OutputVector: transaction.SerializeOutputs(),
+ Locktime: transaction.SerializeLocktime(),
+ }
+ sweepProof := tbtcabi.BitcoinTxProof2{
+ MerkleProof: proof.MerkleProof,
+ TxIndexInBlock: big.NewInt(int64(proof.TxIndexInBlock)),
+ BitcoinHeaders: proof.BitcoinHeaders,
+ CoinbasePreimage: proof.CoinbasePreimage,
+ CoinbaseProof: proof.CoinbaseProof,
+ }
+ utxo := tbtcabi.BitcoinTxUTXO2{
+ TxHash: mainUTXO.Outpoint.TransactionHash,
+ TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
+ TxOutputValue: uint64(mainUTXO.Value),
+ }
+
+ gasEstimate, err := tc.maintainerProxy.SubmitDepositSweepProofGasEstimate(
+ bitcoinTxInfo,
+ sweepProof,
+ utxo,
+ vault,
+ )
+ if err != nil {
+ return err
+ }
+
+ // The original estimate for this contract call is too low and the call
+ // fails on reimbursing the submitter. Example:
+ // 0xe27a92883e0e64da8a3a54a15a260ea2f4d3d48470129ac5c09bfe9637d7e114
+ // Here we add a 20% margin to overcome the gas problems.
+ gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
+
+ _, err = tc.maintainerProxy.SubmitDepositSweepProof(
+ bitcoinTxInfo,
+ sweepProof,
+ utxo,
+ vault,
+ ethutil.TransactionOptions{
+ GasLimit: uint64(gasEstimateWithMargin),
+ },
+ )
+
+ return err
+}
+
+func buildDepositKey(
+ fundingTxHash bitcoin.Hash,
+ fundingOutputIndex uint32,
+) *big.Int {
+ fundingOutputIndexBytes := make([]byte, 4)
+ binary.BigEndian.PutUint32(fundingOutputIndexBytes, fundingOutputIndex)
+
+ depositKey := crypto.Keccak256Hash(
+ append(fundingTxHash[:], fundingOutputIndexBytes...),
+ )
+
+ return depositKey.Big()
+}
+
+func convertDepositSweepProposalToAbiType(
+ walletPublicKeyHash [20]byte,
+ proposal *tbtc.DepositSweepProposal,
+) tbtcabi.WalletProposalValidatorDepositSweepProposal {
+ depositsKeys := make(
+ []tbtcabi.WalletProposalValidatorDepositKey,
+ len(proposal.DepositsKeys),
+ )
+
+ for i, depositKey := range proposal.DepositsKeys {
+ // We can map the depositKey.FundingTxHash field directly to the
+ // [32]byte type. This is because depositKey.FundingTxHash is
+ // a bitcoin.Hash type representing a hash in the
+ // bitcoin.InternalByteOrder, just as the on-chain contract assumes.
+ depositsKeys[i] = tbtcabi.WalletProposalValidatorDepositKey{
+ FundingTxHash: depositKey.FundingTxHash,
+ FundingOutputIndex: depositKey.FundingOutputIndex,
+ }
+ }
+
+ return tbtcabi.WalletProposalValidatorDepositSweepProposal{
+ WalletPubKeyHash: walletPublicKeyHash,
+ DepositsKeys: depositsKeys,
+ SweepTxFee: proposal.SweepTxFee,
+ DepositsRevealBlocks: proposal.DepositsRevealBlocks,
+ }
+}
+
+func (tc *TbtcChain) ValidateDepositSweepProposal(
+ walletPublicKeyHash [20]byte,
+ proposal *tbtc.DepositSweepProposal,
+ depositsExtraInfo []struct {
+ *tbtc.Deposit
+ FundingTx *bitcoin.Transaction
+ },
+) error {
+ dei := make([]tbtcabi.WalletProposalValidatorDepositExtraInfo, len(depositsExtraInfo))
+ for i, depositExtraInfo := range depositsExtraInfo {
+ fundingTx := tbtcabi.BitcoinTxInfo2{
+ Version: depositExtraInfo.FundingTx.SerializeVersion(),
+ InputVector: depositExtraInfo.FundingTx.SerializeInputs(),
+ OutputVector: depositExtraInfo.FundingTx.SerializeOutputs(),
+ Locktime: depositExtraInfo.FundingTx.SerializeLocktime(),
+ }
+
+ dei[i] = tbtcabi.WalletProposalValidatorDepositExtraInfo{
+ FundingTx: fundingTx,
+ BlindingFactor: depositExtraInfo.Deposit.BlindingFactor,
+ WalletPubKeyHash: depositExtraInfo.Deposit.WalletPublicKeyHash,
+ RefundPubKeyHash: depositExtraInfo.Deposit.RefundPublicKeyHash,
+ RefundLocktime: depositExtraInfo.Deposit.RefundLocktime,
+ }
+ }
+
+ valid, err := tc.walletProposalValidator.ValidateDepositSweepProposal(
+ convertDepositSweepProposalToAbiType(walletPublicKeyHash, proposal),
+ dei,
+ )
+ if err != nil {
+ return fmt.Errorf("validation failed: [%v]", err)
+ }
+
+ // Should never happen because `validateDepositSweepProposal` returns true
+ // or reverts (returns an error) but do the check just in case.
+ if !valid {
+ return fmt.Errorf("unexpected validation result")
+ }
+
+ return nil
+}
+
+func (tc *TbtcChain) GetDepositSweepMaxSize() (uint16, error) {
+ return tc.walletProposalValidator.DEPOSITSWEEPMAXSIZE()
+}
+
+func (tc *TbtcChain) GetDepositMinAge() (uint32, error) {
+ return tc.walletProposalValidator.DEPOSITMINAGE()
+}
diff --git a/pkg/chain/ethereum/tbtc_dkg.go b/pkg/chain/ethereum/tbtc_dkg.go
new file mode 100644
index 0000000000..5a786dc3f4
--- /dev/null
+++ b/pkg/chain/ethereum/tbtc_dkg.go
@@ -0,0 +1,555 @@
+package ethereum
+
+import (
+ "crypto/ecdsa"
+ "fmt"
+ "math/big"
+ "reflect"
+ "sort"
+
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil"
+
+ "github.com/keep-network/keep-core/pkg/chain"
+ ecdsaabi "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen/abi"
+ "github.com/keep-network/keep-core/pkg/crypto/secp256k1"
+ "github.com/keep-network/keep-core/pkg/internal/byteutils"
+ "github.com/keep-network/keep-core/pkg/protocol/group"
+ "github.com/keep-network/keep-core/pkg/subscription"
+ "github.com/keep-network/keep-core/pkg/tbtc"
+ "github.com/keep-network/keep-core/pkg/tecdsa/dkg"
+)
+
+func (tc *TbtcChain) OnDKGStarted(
+ handler func(event *tbtc.DKGStartedEvent),
+) subscription.EventSubscription {
+ onEvent := func(
+ seed *big.Int,
+ blockNumber uint64,
+ ) {
+ handler(&tbtc.DKGStartedEvent{
+ Seed: seed,
+ BlockNumber: blockNumber,
+ })
+ }
+
+ return tc.walletRegistry.DkgStartedEvent(nil, nil).OnEvent(onEvent)
+}
+
+func (tc *TbtcChain) PastDKGStartedEvents(
+ filter *tbtc.DKGStartedEventFilter,
+) ([]*tbtc.DKGStartedEvent, error) {
+ var startBlock uint64
+ var endBlock *uint64
+ var seed []*big.Int
+
+ if filter != nil {
+ startBlock = filter.StartBlock
+ endBlock = filter.EndBlock
+ seed = filter.Seed
+ }
+
+ events, err := tc.walletRegistry.PastDkgStartedEvents(
+ startBlock,
+ endBlock,
+ seed,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ dkgStartedEvents := make([]*tbtc.DKGStartedEvent, len(events))
+ for i, event := range events {
+ dkgStartedEvents[i] = &tbtc.DKGStartedEvent{
+ Seed: event.Seed,
+ BlockNumber: event.Raw.BlockNumber,
+ }
+ }
+
+ sort.SliceStable(dkgStartedEvents, func(i, j int) bool {
+ return dkgStartedEvents[i].BlockNumber < dkgStartedEvents[j].BlockNumber
+ })
+
+ return dkgStartedEvents, err
+}
+
+func (tc *TbtcChain) OnDKGResultSubmitted(
+ handler func(event *tbtc.DKGResultSubmittedEvent),
+) subscription.EventSubscription {
+ onEvent := func(
+ resultHash [32]byte,
+ seed *big.Int,
+ result ecdsaabi.EcdsaDkgResult,
+ blockNumber uint64,
+ ) {
+ tbtcResult, err := convertDkgResultFromAbiType(result)
+ if err != nil {
+ logger.Errorf(
+ "unexpected DKG result in DKGResultSubmitted event: [%v]",
+ err,
+ )
+ return
+ }
+
+ handler(&tbtc.DKGResultSubmittedEvent{
+ Seed: seed,
+ ResultHash: resultHash,
+ Result: tbtcResult,
+ BlockNumber: blockNumber,
+ })
+ }
+
+ return tc.walletRegistry.
+ DkgResultSubmittedEvent(nil, nil, nil).
+ OnEvent(onEvent)
+}
+
+// convertDkgResultFromAbiType converts the WalletRegistry-specific DKG
+// result to the format applicable for the TBTC application.
+func convertDkgResultFromAbiType(
+ result ecdsaabi.EcdsaDkgResult,
+) (*tbtc.DKGChainResult, error) {
+ if err := validateMemberIndex(result.SubmitterMemberIndex); err != nil {
+ return nil, fmt.Errorf(
+ "unexpected submitter member index: [%v]",
+ err,
+ )
+ }
+
+ signingMembersIndexes := make(
+ []group.MemberIndex,
+ len(result.SigningMembersIndices),
+ )
+ for i, memberIndex := range result.SigningMembersIndices {
+ if err := validateMemberIndex(memberIndex); err != nil {
+ return nil, fmt.Errorf(
+ "unexpected signing member index: [%v]",
+ err,
+ )
+ }
+
+ signingMembersIndexes[i] = group.MemberIndex(memberIndex.Uint64())
+ }
+
+ return &tbtc.DKGChainResult{
+ SubmitterMemberIndex: group.MemberIndex(result.SubmitterMemberIndex.Uint64()),
+ GroupPublicKey: result.GroupPubKey,
+ MisbehavedMembersIndexes: result.MisbehavedMembersIndices,
+ Signatures: result.Signatures,
+ SigningMembersIndexes: signingMembersIndexes,
+ Members: result.Members,
+ MembersHash: result.MembersHash,
+ }, nil
+}
+
+// convertDkgResultToAbiType converts the TBTC-specific DKG result to
+// the format applicable for the WalletRegistry ABI.
+func convertDkgResultToAbiType(
+ result *tbtc.DKGChainResult,
+) ecdsaabi.EcdsaDkgResult {
+ signingMembersIndices := make([]*big.Int, len(result.SigningMembersIndexes))
+ for i, memberIndex := range result.SigningMembersIndexes {
+ signingMembersIndices[i] = big.NewInt(int64(memberIndex))
+ }
+
+ return ecdsaabi.EcdsaDkgResult{
+ SubmitterMemberIndex: big.NewInt(int64(result.SubmitterMemberIndex)),
+ GroupPubKey: result.GroupPublicKey,
+ MisbehavedMembersIndices: result.MisbehavedMembersIndexes,
+ Signatures: result.Signatures,
+ SigningMembersIndices: signingMembersIndices,
+ Members: result.Members,
+ MembersHash: result.MembersHash,
+ }
+}
+
+func validateMemberIndex(chainMemberIndex *big.Int) error {
+ maxMemberIndex := big.NewInt(group.MaxMemberIndex)
+ if chainMemberIndex.Cmp(maxMemberIndex) > 0 {
+ return fmt.Errorf("invalid member index value: [%v]", chainMemberIndex)
+ }
+
+ return nil
+}
+
+func (tc *TbtcChain) OnDKGResultChallenged(
+ handler func(event *tbtc.DKGResultChallengedEvent),
+) subscription.EventSubscription {
+ onEvent := func(
+ resultHash [32]byte,
+ challenger common.Address,
+ reason string,
+ blockNumber uint64,
+ ) {
+ handler(&tbtc.DKGResultChallengedEvent{
+ ResultHash: resultHash,
+ Challenger: chain.Address(challenger.Hex()),
+ Reason: reason,
+ BlockNumber: blockNumber,
+ })
+ }
+
+ return tc.walletRegistry.
+ DkgResultChallengedEvent(nil, nil, nil).
+ OnEvent(onEvent)
+}
+
+func (tc *TbtcChain) OnDKGResultApproved(
+ handler func(event *tbtc.DKGResultApprovedEvent),
+) subscription.EventSubscription {
+ onEvent := func(
+ resultHash [32]byte,
+ approver common.Address,
+ blockNumber uint64,
+ ) {
+ handler(&tbtc.DKGResultApprovedEvent{
+ ResultHash: resultHash,
+ Approver: chain.Address(approver.Hex()),
+ BlockNumber: blockNumber,
+ })
+ }
+
+ return tc.walletRegistry.
+ DkgResultApprovedEvent(nil, nil, nil).
+ OnEvent(onEvent)
+}
+
+// AssembleDKGResult assembles the DKG chain result according to the rules
+// expected by the given chain.
+func (tc *TbtcChain) AssembleDKGResult(
+ submitterMemberIndex group.MemberIndex,
+ groupPublicKey *ecdsa.PublicKey,
+ operatingMembersIndexes []group.MemberIndex,
+ misbehavedMembersIndexes []group.MemberIndex,
+ signatures map[group.MemberIndex][]byte,
+ groupSelectionResult *tbtc.GroupSelectionResult,
+) (*tbtc.DKGChainResult, error) {
+ serializedGroupPublicKey, err := convertPubKeyToChainFormat(groupPublicKey)
+ if err != nil {
+ return nil, fmt.Errorf(
+ "could not convert group public key to chain format: [%v]",
+ err,
+ )
+ }
+
+ // Sort misbehavedMembersIndexes slice in ascending order as expected
+ // by the on-chain contract.
+ sort.Slice(misbehavedMembersIndexes[:], func(i, j int) bool {
+ return misbehavedMembersIndexes[i] < misbehavedMembersIndexes[j]
+ })
+
+ signingMemberIndices, signatureBytes, err := convertSignaturesToChainFormat(
+ signatures,
+ )
+ if err != nil {
+ return nil, fmt.Errorf(
+ "could not convert signatures to chain format: [%v]",
+ err,
+ )
+ }
+
+ // Sort operatingOperatorsIDs slice in ascending order as the slice
+ // holding the operators IDs used to compute the members hash is
+ // expected to be sorted in the same way.
+ sort.Slice(operatingMembersIndexes[:], func(i, j int) bool {
+ return operatingMembersIndexes[i] < operatingMembersIndexes[j]
+ })
+
+ operatingOperatorsIDs := make([]chain.OperatorID, len(operatingMembersIndexes))
+ for i, operatingMemberIndex := range operatingMembersIndexes {
+ operatingOperatorsIDs[i] =
+ groupSelectionResult.OperatorsIDs[operatingMemberIndex-1]
+ }
+
+ membersHash, err := computeOperatorsIDsHash(operatingOperatorsIDs)
+ if err != nil {
+ return nil, fmt.Errorf("could not compute members hash: [%v]", err)
+ }
+
+ return &tbtc.DKGChainResult{
+ SubmitterMemberIndex: submitterMemberIndex,
+ GroupPublicKey: serializedGroupPublicKey[:],
+ MisbehavedMembersIndexes: misbehavedMembersIndexes,
+ Signatures: signatureBytes,
+ SigningMembersIndexes: signingMemberIndices,
+ Members: groupSelectionResult.OperatorsIDs,
+ MembersHash: membersHash,
+ }, nil
+}
+
+func (tc *TbtcChain) SubmitDKGResult(
+ dkgResult *tbtc.DKGChainResult,
+) error {
+ _, err := tc.walletRegistry.SubmitDkgResult(
+ convertDkgResultToAbiType(dkgResult),
+ )
+
+ return err
+}
+
+// computeOperatorsIDsHash computes the keccak256 hash for the given list
+// of operators IDs.
+func computeOperatorsIDsHash(operatorsIDs chain.OperatorIDs) ([32]byte, error) {
+ uint32SliceType, err := abi.NewType("uint32[]", "uint32[]", nil)
+ if err != nil {
+ return [32]byte{}, err
+ }
+
+ bytes, err := abi.Arguments{{Type: uint32SliceType}}.Pack(operatorsIDs)
+ if err != nil {
+ return [32]byte{}, err
+ }
+
+ return crypto.Keccak256Hash(bytes), nil
+}
+
+// convertSignaturesToChainFormat converts signatures map to two slices. The
+// first slice contains indices of members from the map, sorted in ascending order
+// as required by the contract. The second slice is a slice of concatenated
+// signatures. Signatures and member indices are returned in the matching order.
+// It requires each signature to be exactly 65-byte long.
+func convertSignaturesToChainFormat(
+ signatures map[group.MemberIndex][]byte,
+) ([]group.MemberIndex, []byte, error) {
+ membersIndexes := make([]group.MemberIndex, 0)
+ for memberIndex := range signatures {
+ membersIndexes = append(membersIndexes, memberIndex)
+ }
+
+ sort.Slice(membersIndexes, func(i, j int) bool {
+ return membersIndexes[i] < membersIndexes[j]
+ })
+
+ signatureSize := 65
+
+ var signaturesSlice []byte
+
+ for _, memberIndex := range membersIndexes {
+ signature := signatures[memberIndex]
+
+ if len(signature) != signatureSize {
+ return nil, nil, fmt.Errorf(
+ "invalid signature size for member [%v] got [%d] bytes but [%d] bytes required",
+ memberIndex,
+ len(signature),
+ signatureSize,
+ )
+ }
+
+ signaturesSlice = append(signaturesSlice, signature...)
+ }
+
+ return membersIndexes, signaturesSlice, nil
+}
+
+// convertPubKeyToChainFormat takes X and Y coordinates of a signer's public key
+// and concatenates it to a 64-byte long array. If any of coordinates is shorter
+// than 32-byte it is preceded with zeros.
+func convertPubKeyToChainFormat(publicKey *ecdsa.PublicKey) ([64]byte, error) {
+ var serialized [64]byte
+
+ x, err := byteutils.LeftPadTo32Bytes(publicKey.X.Bytes())
+ if err != nil {
+ return serialized, err
+ }
+
+ y, err := byteutils.LeftPadTo32Bytes(publicKey.Y.Bytes())
+ if err != nil {
+ return serialized, err
+ }
+
+ serializedBytes := append(x, y...)
+
+ copy(serialized[:], serializedBytes)
+
+ return serialized, nil
+}
+
+func (tc *TbtcChain) GetDKGState() (tbtc.DKGState, error) {
+ walletCreationState, err := tc.walletRegistry.GetWalletCreationState()
+ if err != nil {
+ return 0, err
+ }
+
+ var state tbtc.DKGState
+
+ switch walletCreationState {
+ case 0:
+ state = tbtc.Idle
+ case 1:
+ state = tbtc.AwaitingSeed
+ case 2:
+ state = tbtc.AwaitingResult
+ case 3:
+ state = tbtc.Challenge
+ default:
+ err = fmt.Errorf(
+ "unexpected wallet creation state: [%v]",
+ walletCreationState,
+ )
+ }
+
+ return state, err
+}
+
+// CalculateDKGResultSignatureHash calculates a 32-byte hash that is used
+// to produce a signature supporting the given groupPublicKey computed
+// as result of the given DKG process. The misbehavedMembersIndexes parameter
+// should contain indexes of members that were considered as misbehaved
+// during the DKG process. The startBlock argument is the block at which
+// the given DKG process started.
+func (tc *TbtcChain) CalculateDKGResultSignatureHash(
+ groupPublicKey *ecdsa.PublicKey,
+ misbehavedMembersIndexes []group.MemberIndex,
+ startBlock uint64,
+) (dkg.ResultSignatureHash, error) {
+ groupPublicKeyBytes := secp256k1.Marshal(groupPublicKey)
+ // Crop the 04 prefix as the calculateDKGResultSignatureHash function
+ // expects an unprefixed 64-byte public key,
+ unprefixedGroupPublicKeyBytes := groupPublicKeyBytes[1:]
+
+ // Sort misbehavedMembersIndexes slice in ascending order as expected
+ // by the calculateDKGResultSignatureHash function.
+ sort.Slice(misbehavedMembersIndexes[:], func(i, j int) bool {
+ return misbehavedMembersIndexes[i] < misbehavedMembersIndexes[j]
+ })
+
+ return calculateDKGResultSignatureHash(
+ tc.chainID,
+ unprefixedGroupPublicKeyBytes,
+ misbehavedMembersIndexes,
+ big.NewInt(int64(startBlock)),
+ )
+}
+
+// calculateDKGResultSignatureHash computes the keccak256 hash for the given DKG
+// result parameters. It expects that the groupPublicKey is a 64-byte uncompressed
+// public key without the 04 prefix and misbehavedMembersIndexes slice is
+// sorted in ascending order. Those expectations are forced by the contract.
+func calculateDKGResultSignatureHash(
+ chainID *big.Int,
+ groupPublicKey []byte,
+ misbehavedMembersIndexes []group.MemberIndex,
+ startBlock *big.Int,
+) (dkg.ResultSignatureHash, error) {
+ publicKeySize := 64
+
+ if len(groupPublicKey) != publicKeySize {
+ return dkg.ResultSignatureHash{}, fmt.Errorf(
+ "wrong group public key length",
+ )
+ }
+
+ uint256Type, err := abi.NewType("uint256", "uint256", nil)
+ if err != nil {
+ return dkg.ResultSignatureHash{}, err
+ }
+ bytesType, err := abi.NewType("bytes", "bytes", nil)
+ if err != nil {
+ return dkg.ResultSignatureHash{}, err
+ }
+ uint8SliceType, err := abi.NewType("uint8[]", "uint8[]", nil)
+ if err != nil {
+ return dkg.ResultSignatureHash{}, err
+ }
+
+ bytes, err := abi.Arguments{
+ {Type: uint256Type},
+ {Type: bytesType},
+ {Type: uint8SliceType},
+ {Type: uint256Type},
+ }.Pack(
+ chainID,
+ groupPublicKey,
+ misbehavedMembersIndexes,
+ startBlock,
+ )
+ if err != nil {
+ return dkg.ResultSignatureHash{}, err
+ }
+
+ return dkg.ResultSignatureHash(crypto.Keccak256Hash(bytes)), nil
+}
+
+func (tc *TbtcChain) IsDKGResultValid(
+ dkgResult *tbtc.DKGChainResult,
+) (bool, error) {
+ outcome, err := tc.walletRegistry.IsDkgResultValid(
+ convertDkgResultToAbiType(dkgResult),
+ )
+ if err != nil {
+ return false, fmt.Errorf("cannot check result validity: [%v]", err)
+ }
+
+ return parseDkgResultValidationOutcome(&outcome)
+}
+
+// parseDkgResultValidationOutcome parses the DKG validation outcome and returns
+// a boolean indicating whether the result is valid or not. The outcome parameter
+// must be a pointer to a struct containing a boolean flag as the first field.
+//
+// TODO: Find a better way to get the validity flag. This would require changes
+// in the contracts binding generator.
+func parseDkgResultValidationOutcome(
+ outcome interface{},
+) (bool, error) {
+ value := reflect.ValueOf(outcome)
+ switch value.Kind() {
+ case reflect.Pointer:
+ default:
+ return false, fmt.Errorf("result validation outcome is not a pointer")
+ }
+
+ field := value.Elem().Field(0)
+ switch field.Kind() {
+ case reflect.Bool:
+ return field.Bool(), nil
+ default:
+ return false, fmt.Errorf("cannot parse result validation outcome")
+ }
+}
+
+func (tc *TbtcChain) ChallengeDKGResult(dkgResult *tbtc.DKGChainResult) error {
+ _, err := tc.walletRegistry.ChallengeDkgResult(
+ convertDkgResultToAbiType(dkgResult),
+ )
+
+ return err
+}
+
+func (tc *TbtcChain) ApproveDKGResult(dkgResult *tbtc.DKGChainResult) error {
+ result := convertDkgResultToAbiType(dkgResult)
+
+ gasEstimate, err := tc.walletRegistry.ApproveDkgResultGasEstimate(result)
+ if err != nil {
+ return err
+ }
+
+ // The original estimate for this contract call turned out to be too low.
+ // Here we add a 20% margin to overcome the gas problems.
+ gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
+
+ _, err = tc.walletRegistry.ApproveDkgResult(
+ result,
+ ethutil.TransactionOptions{
+ GasLimit: uint64(gasEstimateWithMargin),
+ },
+ )
+
+ return err
+}
+
+func (tc *TbtcChain) DKGParameters() (*tbtc.DKGParameters, error) {
+ parameters, err := tc.walletRegistry.DkgParameters()
+ if err != nil {
+ return nil, err
+ }
+
+ return &tbtc.DKGParameters{
+ SubmissionTimeoutBlocks: parameters.ResultSubmissionTimeout.Uint64(),
+ ChallengePeriodBlocks: parameters.ResultChallengePeriodLength.Uint64(),
+ ApprovePrecedencePeriodBlocks: parameters.SubmitterPrecedencePeriodLength.Uint64(),
+ }, nil
+}
diff --git a/pkg/chain/ethereum/tbtc_inactivity.go b/pkg/chain/ethereum/tbtc_inactivity.go
new file mode 100644
index 0000000000..a4987ab121
--- /dev/null
+++ b/pkg/chain/ethereum/tbtc_inactivity.go
@@ -0,0 +1,195 @@
+package ethereum
+
+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/chain"
+ ecdsaabi "github.com/keep-network/keep-core/pkg/chain/ethereum/ecdsa/gen/abi"
+ "github.com/keep-network/keep-core/pkg/crypto/secp256k1"
+ "github.com/keep-network/keep-core/pkg/protocol/group"
+ "github.com/keep-network/keep-core/pkg/protocol/inactivity"
+ "github.com/keep-network/keep-core/pkg/subscription"
+ "github.com/keep-network/keep-core/pkg/tbtc"
+)
+
+func (tc *TbtcChain) OnInactivityClaimed(
+ handler func(event *tbtc.InactivityClaimedEvent),
+) subscription.EventSubscription {
+ onEvent := func(
+ walletID [32]byte,
+ nonce *big.Int,
+ notifier common.Address,
+ blockNumber uint64,
+ ) {
+ handler(&tbtc.InactivityClaimedEvent{
+ WalletID: walletID,
+ Nonce: nonce,
+ Notifier: chain.Address(notifier.Hex()),
+ BlockNumber: blockNumber,
+ })
+ }
+
+ return tc.walletRegistry.InactivityClaimedEvent(nil, nil).OnEvent(onEvent)
+}
+
+func (tc *TbtcChain) AssembleInactivityClaim(
+ walletID [32]byte,
+ inactiveMembersIndices []group.MemberIndex,
+ signatures map[group.MemberIndex][]byte,
+ heartbeatFailed bool,
+) (
+ *tbtc.InactivityClaim,
+ error,
+) {
+ signingMemberIndices, signatureBytes, err := convertSignaturesToChainFormat(
+ signatures,
+ )
+ if err != nil {
+ return nil, fmt.Errorf(
+ "could not convert signatures to chain format: [%v]",
+ err,
+ )
+ }
+
+ return &tbtc.InactivityClaim{
+ WalletID: walletID,
+ InactiveMembersIndices: inactiveMembersIndices,
+ HeartbeatFailed: heartbeatFailed,
+ Signatures: signatureBytes,
+ SigningMembersIndices: signingMemberIndices,
+ }, nil
+}
+
+// convertInactivityClaimToAbiType converts the TBTC-specific inactivity claim
+// to the format applicable for the WalletRegistry ABI.
+func convertInactivityClaimToAbiType(
+ claim *tbtc.InactivityClaim,
+) ecdsaabi.EcdsaInactivityClaim {
+ inactiveMembersIndices := make([]*big.Int, len(claim.InactiveMembersIndices))
+ for i, memberIndex := range claim.InactiveMembersIndices {
+ inactiveMembersIndices[i] = big.NewInt(int64(memberIndex))
+ }
+
+ signingMembersIndices := make([]*big.Int, len(claim.SigningMembersIndices))
+ for i, memberIndex := range claim.SigningMembersIndices {
+ signingMembersIndices[i] = big.NewInt(int64(memberIndex))
+ }
+
+ return ecdsaabi.EcdsaInactivityClaim{
+ WalletID: claim.WalletID,
+ InactiveMembersIndices: inactiveMembersIndices,
+ HeartbeatFailed: claim.HeartbeatFailed,
+ Signatures: claim.Signatures,
+ SigningMembersIndices: signingMembersIndices,
+ }
+}
+
+func (tc *TbtcChain) SubmitInactivityClaim(
+ claim *tbtc.InactivityClaim,
+ nonce *big.Int,
+ groupMembers []uint32,
+) error {
+ _, err := tc.walletRegistry.NotifyOperatorInactivity(
+ convertInactivityClaimToAbiType(claim),
+ nonce,
+ groupMembers,
+ )
+
+ return err
+}
+
+func (tc *TbtcChain) CalculateInactivityClaimHash(
+ claim *inactivity.ClaimPreimage,
+) (inactivity.ClaimHash, error) {
+ walletPublicKeyBytes := secp256k1.Marshal(claim.WalletPublicKey)
+ // Crop the 04 prefix as the calculateInactivityClaimHash function expects
+ // an unprefixed 64-byte public key,
+ unprefixedGroupPublicKeyBytes := walletPublicKeyBytes[1:]
+
+ // The type representing inactive member index should be `big.Int` as the
+ // smart contract reading the calculated hash uses `uint256` for inactive
+ // member indexes.
+ inactiveMembersIndexes := make([]*big.Int, len(claim.InactiveMembersIndexes))
+ for i, index := range claim.InactiveMembersIndexes {
+ inactiveMembersIndexes[i] = big.NewInt(int64(index))
+ }
+
+ return calculateInactivityClaimHash(
+ tc.chainID,
+ claim.Nonce,
+ unprefixedGroupPublicKeyBytes,
+ inactiveMembersIndexes,
+ claim.HeartbeatFailed,
+ )
+}
+
+func calculateInactivityClaimHash(
+ chainID *big.Int,
+ nonce *big.Int,
+ walletPublicKey []byte,
+ inactiveMembersIndexes []*big.Int,
+ heartbeatFailed bool,
+) (inactivity.ClaimHash, error) {
+ publicKeySize := 64
+
+ if len(walletPublicKey) != publicKeySize {
+ return inactivity.ClaimHash{}, fmt.Errorf(
+ "wrong wallet public key length",
+ )
+ }
+
+ uint256Type, err := abi.NewType("uint256", "uint256", nil)
+ if err != nil {
+ return inactivity.ClaimHash{}, err
+ }
+ bytesType, err := abi.NewType("bytes", "bytes", nil)
+ if err != nil {
+ return inactivity.ClaimHash{}, err
+ }
+ uint256SliceType, err := abi.NewType("uint256[]", "uint256[]", nil)
+ if err != nil {
+ return inactivity.ClaimHash{}, err
+ }
+ boolType, err := abi.NewType("bool", "bool", nil)
+ if err != nil {
+ return inactivity.ClaimHash{}, err
+ }
+
+ bytes, err := abi.Arguments{
+ {Type: uint256Type},
+ {Type: uint256Type},
+ {Type: bytesType},
+ {Type: uint256SliceType},
+ {Type: boolType},
+ }.Pack(
+ chainID,
+ nonce,
+ walletPublicKey,
+ inactiveMembersIndexes,
+ heartbeatFailed,
+ )
+ if err != nil {
+ return inactivity.ClaimHash{}, err
+ }
+
+ return inactivity.ClaimHash(crypto.Keccak256Hash(bytes)), nil
+}
+
+func (tc *TbtcChain) GetInactivityClaimNonce(
+ walletID [32]byte,
+) (*big.Int, error) {
+ nonce, err := tc.walletRegistry.InactivityClaimNonce(walletID)
+ if err != nil {
+ return nil, fmt.Errorf(
+ "failed to get inactivity claim nonce: [%w]",
+ err,
+ )
+ }
+
+ return nonce, nil
+}
diff --git a/pkg/chain/ethereum/tbtc_moving_funds.go b/pkg/chain/ethereum/tbtc_moving_funds.go
new file mode 100644
index 0000000000..959db4e546
--- /dev/null
+++ b/pkg/chain/ethereum/tbtc_moving_funds.go
@@ -0,0 +1,408 @@
+package ethereum
+
+import (
+ "encoding/binary"
+ "fmt"
+ "math/big"
+ "sort"
+ "time"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil"
+ "github.com/keep-network/keep-core/pkg/bitcoin"
+
+ "github.com/keep-network/keep-core/pkg/chain"
+ tbtcabi "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi"
+ "github.com/keep-network/keep-core/pkg/tbtc"
+)
+
+func (tc *TbtcChain) ComputeMovingFundsCommitmentHash(
+ targetWallets [][20]byte,
+) [32]byte {
+ return computeMovingFundsCommitmentHash(targetWallets)
+}
+
+func computeMovingFundsCommitmentHash(targetWallets [][20]byte) [32]byte {
+ packedWallets := []byte{}
+
+ for _, wallet := range targetWallets {
+ packedWallets = append(packedWallets, wallet[:]...)
+ // Each wallet hash must be padded with 12 zero bytes following the
+ // actual hash.
+ packedWallets = append(packedWallets, make([]byte, 12)...)
+ }
+
+ return crypto.Keccak256Hash(packedWallets)
+}
+
+func (tc *TbtcChain) PastMovingFundsCommitmentSubmittedEvents(
+ filter *tbtc.MovingFundsCommitmentSubmittedEventFilter,
+) ([]*tbtc.MovingFundsCommitmentSubmittedEvent, error) {
+ var startBlock uint64
+ var endBlock *uint64
+ var walletPublicKeyHash [][20]byte
+
+ if filter != nil {
+ startBlock = filter.StartBlock
+ endBlock = filter.EndBlock
+ walletPublicKeyHash = filter.WalletPublicKeyHash
+ }
+
+ events, err := tc.bridge.PastMovingFundsCommitmentSubmittedEvents(
+ startBlock,
+ endBlock,
+ walletPublicKeyHash,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ convertedEvents := make([]*tbtc.MovingFundsCommitmentSubmittedEvent, 0)
+ for _, event := range events {
+ convertedEvent := &tbtc.MovingFundsCommitmentSubmittedEvent{
+ WalletPublicKeyHash: event.WalletPubKeyHash,
+ TargetWallets: event.TargetWallets,
+ Submitter: chain.Address(event.Submitter.Hex()),
+ BlockNumber: event.Raw.BlockNumber,
+ }
+
+ convertedEvents = append(convertedEvents, convertedEvent)
+ }
+
+ sort.SliceStable(
+ convertedEvents,
+ func(i, j int) bool {
+ return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
+ },
+ )
+
+ return convertedEvents, err
+}
+
+func (tc *TbtcChain) PastMovingFundsCompletedEvents(
+ filter *tbtc.MovingFundsCompletedEventFilter,
+) ([]*tbtc.MovingFundsCompletedEvent, error) {
+ var startBlock uint64
+ var endBlock *uint64
+ var walletPublicKeyHash [][20]byte
+
+ if filter != nil {
+ startBlock = filter.StartBlock
+ endBlock = filter.EndBlock
+ walletPublicKeyHash = filter.WalletPublicKeyHash
+ }
+
+ events, err := tc.bridge.PastMovingFundsCompletedEvents(
+ startBlock,
+ endBlock,
+ walletPublicKeyHash,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ convertedEvents := make([]*tbtc.MovingFundsCompletedEvent, 0)
+ for _, event := range events {
+ convertedEvent := &tbtc.MovingFundsCompletedEvent{
+ WalletPublicKeyHash: event.WalletPubKeyHash,
+ MovingFundsTxHash: event.MovingFundsTxHash,
+ BlockNumber: event.Raw.BlockNumber,
+ }
+
+ convertedEvents = append(convertedEvents, convertedEvent)
+ }
+
+ sort.SliceStable(
+ convertedEvents,
+ func(i, j int) bool {
+ return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
+ },
+ )
+
+ return convertedEvents, err
+}
+
+func (tc *TbtcChain) SubmitMovingFundsCommitment(
+ walletPublicKeyHash [20]byte,
+ walletMainUTXO bitcoin.UnspentTransactionOutput,
+ walletMembersIDs []uint32,
+ walletMemberIndex uint32,
+ targetWallets [][20]byte,
+) error {
+ mainUtxo := tbtcabi.BitcoinTxUTXO{
+ TxHash: walletMainUTXO.Outpoint.TransactionHash,
+ TxOutputIndex: walletMainUTXO.Outpoint.OutputIndex,
+ TxOutputValue: uint64(walletMainUTXO.Value),
+ }
+ _, err := tc.bridge.SubmitMovingFundsCommitment(
+ walletPublicKeyHash,
+ mainUtxo,
+ walletMembersIDs,
+ big.NewInt(int64(walletMemberIndex)),
+ targetWallets,
+ )
+ return err
+}
+
+func (tc *TbtcChain) SubmitMovingFundsProofWithReimbursement(
+ transaction *bitcoin.Transaction,
+ proof *bitcoin.SpvProof,
+ mainUTXO bitcoin.UnspentTransactionOutput,
+ walletPublicKeyHash [20]byte,
+) error {
+ bitcoinTxInfo := tbtcabi.BitcoinTxInfo3{
+ Version: transaction.SerializeVersion(),
+ InputVector: transaction.SerializeInputs(),
+ OutputVector: transaction.SerializeOutputs(),
+ Locktime: transaction.SerializeLocktime(),
+ }
+ movingFundsProof := tbtcabi.BitcoinTxProof2{
+ MerkleProof: proof.MerkleProof,
+ TxIndexInBlock: big.NewInt(int64(proof.TxIndexInBlock)),
+ BitcoinHeaders: proof.BitcoinHeaders,
+ CoinbasePreimage: proof.CoinbasePreimage,
+ CoinbaseProof: proof.CoinbaseProof,
+ }
+ utxo := tbtcabi.BitcoinTxUTXO2{
+ TxHash: mainUTXO.Outpoint.TransactionHash,
+ TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
+ TxOutputValue: uint64(mainUTXO.Value),
+ }
+
+ gasEstimate, err := tc.maintainerProxy.SubmitMovingFundsProofGasEstimate(
+ bitcoinTxInfo,
+ movingFundsProof,
+ utxo,
+ walletPublicKeyHash,
+ )
+ if err != nil {
+ return err
+ }
+
+ // The original estimate for this contract call is too low and the call
+ // fails on reimbursing the submitter. Example:
+ // 0xe27a92883e0e64da8a3a54a15a260ea2f4d3d48470129ac5c09bfe9637d7e114
+ // Here we add a 20% margin to overcome the gas problems.
+ gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
+
+ _, err = tc.maintainerProxy.SubmitMovingFundsProof(
+ bitcoinTxInfo,
+ movingFundsProof,
+ utxo,
+ walletPublicKeyHash,
+ ethutil.TransactionOptions{
+ GasLimit: uint64(gasEstimateWithMargin),
+ },
+ )
+
+ return err
+}
+
+func (tc *TbtcChain) SubmitMovedFundsSweepProofWithReimbursement(
+ transaction *bitcoin.Transaction,
+ proof *bitcoin.SpvProof,
+ mainUTXO bitcoin.UnspentTransactionOutput,
+) error {
+ bitcoinTxInfo := tbtcabi.BitcoinTxInfo3{
+ Version: transaction.SerializeVersion(),
+ InputVector: transaction.SerializeInputs(),
+ OutputVector: transaction.SerializeOutputs(),
+ Locktime: transaction.SerializeLocktime(),
+ }
+ movedFundsSweepProof := tbtcabi.BitcoinTxProof2{
+ MerkleProof: proof.MerkleProof,
+ TxIndexInBlock: big.NewInt(int64(proof.TxIndexInBlock)),
+ BitcoinHeaders: proof.BitcoinHeaders,
+ CoinbasePreimage: proof.CoinbasePreimage,
+ CoinbaseProof: proof.CoinbaseProof,
+ }
+ utxo := tbtcabi.BitcoinTxUTXO2{
+ TxHash: mainUTXO.Outpoint.TransactionHash,
+ TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
+ TxOutputValue: uint64(mainUTXO.Value),
+ }
+
+ gasEstimate, err := tc.maintainerProxy.SubmitMovedFundsSweepProofGasEstimate(
+ bitcoinTxInfo,
+ movedFundsSweepProof,
+ utxo,
+ )
+ if err != nil {
+ return err
+ }
+
+ // The original estimate for this contract call is too low and the call
+ // fails on reimbursing the submitter. Example:
+ // 0xe27a92883e0e64da8a3a54a15a260ea2f4d3d48470129ac5c09bfe9637d7e114
+ // Here we add a 20% margin to overcome the gas problems.
+ gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
+
+ _, err = tc.maintainerProxy.SubmitMovedFundsSweepProof(
+ bitcoinTxInfo,
+ movedFundsSweepProof,
+ utxo,
+ ethutil.TransactionOptions{
+ GasLimit: uint64(gasEstimateWithMargin),
+ },
+ )
+
+ return err
+}
+
+func (tc *TbtcChain) ValidateMovedFundsSweepProposal(
+ walletPublicKeyHash [20]byte,
+ proposal *tbtc.MovedFundsSweepProposal,
+) error {
+ abiProposal := tbtcabi.WalletProposalValidatorMovedFundsSweepProposal{
+ WalletPubKeyHash: walletPublicKeyHash,
+ MovingFundsTxHash: proposal.MovingFundsTxHash,
+ MovingFundsTxOutputIndex: proposal.MovingFundsTxOutputIndex,
+ MovedFundsSweepTxFee: proposal.SweepTxFee,
+ }
+
+ valid, err := tc.walletProposalValidator.ValidateMovedFundsSweepProposal(
+ abiProposal,
+ )
+ if err != nil {
+ return fmt.Errorf("validation failed: [%v]", err)
+ }
+
+ // Should never happen because `validateMovedFundsSweepProposal` returns
+ // true or reverts (returns an error) but do the check just in case.
+ if !valid {
+ return fmt.Errorf("unexpected validation result")
+ }
+
+ return nil
+}
+
+func (tc *TbtcChain) GetMovingFundsParameters() (tbtc.MovingFundsParameters, error) {
+ parameters, err := tc.bridge.MovingFundsParameters()
+ if err != nil {
+ return tbtc.MovingFundsParameters{}, err
+ }
+
+ return tbtc.MovingFundsParameters{
+ TxMaxTotalFee: parameters.MovingFundsTxMaxTotalFee,
+ DustThreshold: parameters.MovingFundsDustThreshold,
+ TimeoutResetDelay: parameters.MovingFundsTimeoutResetDelay,
+ Timeout: parameters.MovingFundsTimeout,
+ TimeoutSlashingAmount: parameters.MovingFundsTimeoutSlashingAmount,
+ TimeoutNotifierRewardMultiplier: parameters.MovingFundsTimeoutNotifierRewardMultiplier,
+ CommitmentGasOffset: parameters.MovingFundsCommitmentGasOffset,
+ SweepTxMaxTotalFee: parameters.MovedFundsSweepTxMaxTotalFee,
+ SweepTimeout: parameters.MovedFundsSweepTimeout,
+ SweepTimeoutSlashingAmount: parameters.MovedFundsSweepTimeoutSlashingAmount,
+ SweepTimeoutNotifierRewardMultiplier: parameters.MovedFundsSweepTimeoutNotifierRewardMultiplier,
+ }, nil
+}
+
+func (tc *TbtcChain) GetMovedFundsSweepRequest(
+ movingFundsTxHash bitcoin.Hash,
+ movingFundsTxOutpointIndex uint32,
+) (*tbtc.MovedFundsSweepRequest, bool, error) {
+ movedFundsKey := buildMovedFundsKey(
+ movingFundsTxHash,
+ movingFundsTxOutpointIndex,
+ )
+
+ movedFundsSweepRequest, err := tc.bridge.MovedFundsSweepRequests(
+ movedFundsKey,
+ )
+ if err != nil {
+ return nil, false, fmt.Errorf(
+ "cannot get moved funds sweep request for key [0x%x]: [%v]",
+ movedFundsKey.Text(16),
+ err,
+ )
+ }
+
+ // Moved funds sweep request not found.
+ if movedFundsSweepRequest.CreatedAt == 0 {
+ return nil, false, nil
+ }
+
+ state, err := parseMovedFundsSweepRequestState(movedFundsSweepRequest.State)
+ if err != nil {
+ return nil, false, fmt.Errorf(
+ "cannot parse state for moved funds sweep request [0x%x]: [%v]",
+ movedFundsKey.Text(16),
+ err,
+ )
+ }
+
+ return &tbtc.MovedFundsSweepRequest{
+ WalletPublicKeyHash: movedFundsSweepRequest.WalletPubKeyHash,
+ Value: movedFundsSweepRequest.Value,
+ CreatedAt: time.Unix(int64(movedFundsSweepRequest.CreatedAt), 0),
+ State: state,
+ }, true, nil
+}
+
+func parseMovedFundsSweepRequestState(value uint8) (
+ tbtc.MovedFundsSweepRequestState,
+ error,
+) {
+ switch value {
+ case 0:
+ return tbtc.MovedFundsStateUnknown, nil
+ case 1:
+ return tbtc.MovedFundsStatePending, nil
+ case 2:
+ return tbtc.MovedFundsStateProcessed, nil
+ case 3:
+ return tbtc.MovedFundsStateTimedOut, nil
+ default:
+ return 0, fmt.Errorf(
+ "unexpected moved funds sweep request state value: [%v]",
+ value,
+ )
+ }
+}
+
+func buildMovedFundsKey(
+ movingFundsTxHash bitcoin.Hash,
+ movingFundsTxOutpointIndex uint32,
+) *big.Int {
+ indexBytes := make([]byte, 4)
+ binary.BigEndian.PutUint32(indexBytes, movingFundsTxOutpointIndex)
+
+ movedFundsKey := crypto.Keccak256Hash(
+ append(movingFundsTxHash[:], indexBytes...),
+ )
+
+ return movedFundsKey.Big()
+}
+
+func (tc *TbtcChain) ValidateMovingFundsProposal(
+ walletPublicKeyHash [20]byte,
+ mainUTXO *bitcoin.UnspentTransactionOutput,
+ proposal *tbtc.MovingFundsProposal,
+) error {
+ abiProposal := tbtcabi.WalletProposalValidatorMovingFundsProposal{
+ WalletPubKeyHash: walletPublicKeyHash,
+ TargetWallets: proposal.TargetWallets,
+ MovingFundsTxFee: proposal.MovingFundsTxFee,
+ }
+ abiMainUTXO := tbtcabi.BitcoinTxUTXO3{
+ TxHash: mainUTXO.Outpoint.TransactionHash,
+ TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
+ TxOutputValue: uint64(mainUTXO.Value),
+ }
+
+ valid, err := tc.walletProposalValidator.ValidateMovingFundsProposal(
+ abiProposal,
+ abiMainUTXO,
+ )
+ if err != nil {
+ return fmt.Errorf("validation failed: [%v]", err)
+ }
+
+ // Should never happen because `validateMovingFundsProposal` returns true
+ // or reverts (returns an error) but do the check just in case.
+ if !valid {
+ return fmt.Errorf("unexpected validation result")
+ }
+
+ return nil
+}
diff --git a/pkg/chain/ethereum/tbtc_redemption.go b/pkg/chain/ethereum/tbtc_redemption.go
new file mode 100644
index 0000000000..9830a4f554
--- /dev/null
+++ b/pkg/chain/ethereum/tbtc_redemption.go
@@ -0,0 +1,297 @@
+package ethereum
+
+import (
+ "fmt"
+ "math/big"
+ "sort"
+ "time"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil"
+ "github.com/keep-network/keep-core/pkg/bitcoin"
+
+ "github.com/keep-network/keep-core/pkg/chain"
+ tbtcabi "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi"
+ "github.com/keep-network/keep-core/pkg/tbtc"
+)
+
+func (tc *TbtcChain) PastRedemptionRequestedEvents(
+ filter *tbtc.RedemptionRequestedEventFilter,
+) ([]*tbtc.RedemptionRequestedEvent, error) {
+ var startBlock uint64
+ var endBlock *uint64
+ var redeemers []common.Address
+ var walletPublicKeyHash [][20]byte
+
+ if filter != nil {
+ startBlock = filter.StartBlock
+ endBlock = filter.EndBlock
+
+ for _, r := range filter.Redeemer {
+ redeemers = append(redeemers, common.HexToAddress(r.String()))
+ }
+
+ walletPublicKeyHash = filter.WalletPublicKeyHash
+ }
+
+ events, err := tc.bridge.PastRedemptionRequestedEvents(
+ startBlock,
+ endBlock,
+ walletPublicKeyHash,
+ redeemers,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ convertedEvents := make([]*tbtc.RedemptionRequestedEvent, 0)
+ for _, event := range events {
+ redeemerOutputScript, err := bitcoin.NewScriptFromVarLenData(
+ event.RedeemerOutputScript,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ convertedEvent := &tbtc.RedemptionRequestedEvent{
+ WalletPublicKeyHash: event.WalletPubKeyHash,
+ RedeemerOutputScript: redeemerOutputScript,
+ Redeemer: chain.Address(event.Redeemer.Hex()),
+ RequestedAmount: event.RequestedAmount,
+ TreasuryFee: event.TreasuryFee,
+ TxMaxFee: event.TreasuryFee,
+ BlockNumber: event.Raw.BlockNumber,
+ }
+
+ convertedEvents = append(convertedEvents, convertedEvent)
+ }
+
+ sort.SliceStable(
+ convertedEvents,
+ func(i, j int) bool {
+ return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
+ },
+ )
+
+ return convertedEvents, err
+}
+
+func (tc *TbtcChain) BuildRedemptionKey(
+ walletPublicKeyHash [20]byte,
+ redeemerOutputScript bitcoin.Script,
+) (*big.Int, error) {
+ return buildRedemptionKey(walletPublicKeyHash, redeemerOutputScript)
+}
+
+func (tc *TbtcChain) GetPendingRedemptionRequest(
+ walletPublicKeyHash [20]byte,
+ redeemerOutputScript bitcoin.Script,
+) (*tbtc.RedemptionRequest, bool, error) {
+ redemptionKey, err := buildRedemptionKey(walletPublicKeyHash, redeemerOutputScript)
+ if err != nil {
+ return nil, false, fmt.Errorf("cannot build redemption key: [%v]", err)
+ }
+
+ redemptionRequest, err := tc.bridge.PendingRedemptions(redemptionKey)
+ if err != nil {
+ return nil, false, fmt.Errorf(
+ "cannot get pending redemption request for key [0x%x]: [%v]",
+ redemptionKey.Text(16),
+ err,
+ )
+ }
+
+ // Redemption not found.
+ if redemptionRequest.RequestedAt == 0 {
+ return nil, false, nil
+ }
+
+ return &tbtc.RedemptionRequest{
+ Redeemer: chain.Address(redemptionRequest.Redeemer.Hex()),
+ RedeemerOutputScript: redeemerOutputScript,
+ RequestedAmount: redemptionRequest.RequestedAmount,
+ TreasuryFee: redemptionRequest.TreasuryFee,
+ TxMaxFee: redemptionRequest.TxMaxFee,
+ RequestedAt: time.Unix(int64(redemptionRequest.RequestedAt), 0),
+ }, true, nil
+}
+
+func (tc *TbtcChain) SubmitRedemptionProofWithReimbursement(
+ transaction *bitcoin.Transaction,
+ proof *bitcoin.SpvProof,
+ mainUTXO bitcoin.UnspentTransactionOutput,
+ walletPublicKeyHash [20]byte,
+) error {
+ bitcoinTxInfo := tbtcabi.BitcoinTxInfo3{
+ Version: transaction.SerializeVersion(),
+ InputVector: transaction.SerializeInputs(),
+ OutputVector: transaction.SerializeOutputs(),
+ Locktime: transaction.SerializeLocktime(),
+ }
+ redemptionProof := tbtcabi.BitcoinTxProof2{
+ MerkleProof: proof.MerkleProof,
+ TxIndexInBlock: big.NewInt(int64(proof.TxIndexInBlock)),
+ BitcoinHeaders: proof.BitcoinHeaders,
+ CoinbasePreimage: proof.CoinbasePreimage,
+ CoinbaseProof: proof.CoinbaseProof,
+ }
+ utxo := tbtcabi.BitcoinTxUTXO2{
+ TxHash: mainUTXO.Outpoint.TransactionHash,
+ TxOutputIndex: mainUTXO.Outpoint.OutputIndex,
+ TxOutputValue: uint64(mainUTXO.Value),
+ }
+
+ gasEstimate, err := tc.maintainerProxy.SubmitRedemptionProofGasEstimate(
+ bitcoinTxInfo,
+ redemptionProof,
+ utxo,
+ walletPublicKeyHash,
+ )
+ if err != nil {
+ return err
+ }
+
+ // The original estimate for this contract call is too low and the call
+ // fails on reimbursing the submitter. Example:
+ // 0xe27a92883e0e64da8a3a54a15a260ea2f4d3d48470129ac5c09bfe9637d7e114
+ // Here we add a 20% margin to overcome the gas problems.
+ gasEstimateWithMargin := float64(gasEstimate) * float64(1.2)
+
+ _, err = tc.maintainerProxy.SubmitRedemptionProof(
+ bitcoinTxInfo,
+ redemptionProof,
+ utxo,
+ walletPublicKeyHash,
+ ethutil.TransactionOptions{
+ GasLimit: uint64(gasEstimateWithMargin),
+ },
+ )
+
+ return err
+}
+
+func buildRedemptionKey(
+ walletPublicKeyHash [20]byte,
+ redeemerOutputScript bitcoin.Script,
+) (*big.Int, error) {
+ // The Bridge contract builds the redemption key using the length-prefixed
+ // redeemer output script.
+ prefixedRedeemerOutputScript, err := redeemerOutputScript.ToVarLenData()
+ if err != nil {
+ return nil, fmt.Errorf("cannot build prefixed redeemer output script: [%v]", err)
+ }
+
+ redeemerOutputScriptHash := crypto.Keccak256Hash(prefixedRedeemerOutputScript)
+
+ redemptionKey := crypto.Keccak256Hash(
+ append(redeemerOutputScriptHash[:], walletPublicKeyHash[:]...),
+ )
+
+ return redemptionKey.Big(), nil
+}
+
+func (tc *TbtcChain) GetRedemptionParameters() (tbtc.RedemptionParameters, error) {
+ parameters, err := tc.bridge.RedemptionParameters()
+ if err != nil {
+ return tbtc.RedemptionParameters{}, err
+ }
+
+ return tbtc.RedemptionParameters{
+ DustThreshold: parameters.RedemptionDustThreshold,
+ TreasuryFeeDivisor: parameters.RedemptionTreasuryFeeDivisor,
+ TxMaxFee: parameters.RedemptionTxMaxFee,
+ TxMaxTotalFee: parameters.RedemptionTxMaxTotalFee,
+ Timeout: parameters.RedemptionTimeout,
+ TimeoutSlashingAmount: parameters.RedemptionTimeoutSlashingAmount,
+ TimeoutNotifierRewardMultiplier: parameters.RedemptionTimeoutNotifierRewardMultiplier,
+ }, nil
+}
+
+func (tc *TbtcChain) ValidateRedemptionProposal(
+ walletPublicKeyHash [20]byte,
+ proposal *tbtc.RedemptionProposal,
+) error {
+ abiProposal, err := convertRedemptionProposalToAbiType(
+ walletPublicKeyHash,
+ proposal,
+ )
+ if err != nil {
+ return fmt.Errorf("cannot convert proposal to abi type: [%v]", err)
+ }
+
+ valid, err := tc.walletProposalValidator.ValidateRedemptionProposal(
+ abiProposal,
+ )
+ if err != nil {
+ return fmt.Errorf("validation failed: [%v]", err)
+ }
+
+ // Should never happen because `validateRedemptionProposal` returns true
+ // or reverts (returns an error) but do the check just in case.
+ if !valid {
+ return fmt.Errorf("unexpected validation result")
+ }
+
+ return nil
+}
+
+func convertRedemptionProposalToAbiType(
+ walletPublicKeyHash [20]byte,
+ proposal *tbtc.RedemptionProposal,
+) (tbtcabi.WalletProposalValidatorRedemptionProposal, error) {
+ redeemersOutputScripts := make(
+ [][]byte,
+ len(proposal.RedeemersOutputScripts),
+ )
+
+ for i, script := range proposal.RedeemersOutputScripts {
+ // The on-chain script representation must be prepended with the script's
+ // byte-length while bitcoin.Script is not. We need to add the
+ // length prefix.
+ prefixedScript, err := script.ToVarLenData()
+ if err != nil {
+ return tbtcabi.WalletProposalValidatorRedemptionProposal{}, fmt.Errorf(
+ "cannot convert redeemer output script: [%v]",
+ err,
+ )
+ }
+
+ redeemersOutputScripts[i] = prefixedScript
+ }
+
+ return tbtcabi.WalletProposalValidatorRedemptionProposal{
+ WalletPubKeyHash: walletPublicKeyHash,
+ RedeemersOutputScripts: redeemersOutputScripts,
+ RedemptionTxFee: proposal.RedemptionTxFee,
+ }, nil
+}
+
+func (tc *TbtcChain) GetRedemptionMaxSize() (uint16, error) {
+ return tc.walletProposalValidator.REDEMPTIONMAXSIZE()
+}
+
+func (tc *TbtcChain) GetRedemptionRequestMinAge() (uint32, error) {
+ return tc.walletProposalValidator.REDEMPTIONREQUESTMINAGE()
+}
+
+func (tc *TbtcChain) GetRedemptionDelay(
+ walletPublicKeyHash [20]byte,
+ redeemerOutputScript bitcoin.Script,
+) (time.Duration, error) {
+ if tc.redemptionWatchtower == nil {
+ return 0, nil
+ }
+
+ redemptionKey, err := tc.BuildRedemptionKey(walletPublicKeyHash, redeemerOutputScript)
+ if err != nil {
+ return 0, fmt.Errorf("cannot build redemption key: [%v]", err)
+ }
+
+ delay, err := tc.redemptionWatchtower.GetRedemptionDelay(redemptionKey)
+ if err != nil {
+ return 0, fmt.Errorf("cannot get redemption delay: [%v]", err)
+ }
+
+ return time.Duration(delay) * time.Second, nil
+}
diff --git a/pkg/chain/ethereum/tbtc_sortition.go b/pkg/chain/ethereum/tbtc_sortition.go
new file mode 100644
index 0000000000..01502da357
--- /dev/null
+++ b/pkg/chain/ethereum/tbtc_sortition.go
@@ -0,0 +1,247 @@
+package ethereum
+
+import (
+ "context"
+ "fmt"
+ "math/big"
+
+ "github.com/ethereum/go-ethereum/common"
+
+ "github.com/keep-network/keep-core/pkg/chain"
+ "github.com/keep-network/keep-core/pkg/operator"
+ "github.com/keep-network/keep-core/pkg/tbtc"
+)
+
+// EcdsaWalletGroupParametersFromChain mirrors EcdsaDkgValidator sizing constants
+// when EcdsaDkgValidator contract address was configured under [ethereum]
+// contract addresses or developer.ecdsaDkgValidatorAddress alias. When absent,
+// returns (nil, nil) and callers use defaultGroupParameters(network).
+func (tc *TbtcChain) EcdsaWalletGroupParametersFromChain(
+ ctx context.Context,
+) (*tbtc.GroupParameters, error) {
+ if tc.ecdsaDkgValidatorAddress == (common.Address{}) {
+ return nil, nil
+ }
+ return ecdsaWalletGroupParametersFromValidator(
+ ctx,
+ tc.baseChain.client,
+ tc.ecdsaDkgValidatorAddress,
+ )
+}
+
+// Staking returns address of the TokenStaking contract the WalletRegistry is
+// connected to.
+func (tc *TbtcChain) Staking() (chain.Address, error) {
+ stakingContractAddress, err := tc.walletRegistry.Staking()
+ if err != nil {
+ return "", fmt.Errorf(
+ "failed to get the token staking address: [%w]",
+ err,
+ )
+ }
+
+ return chain.Address(stakingContractAddress.String()), nil
+}
+
+// IsRecognized checks whether the given operator is recognized by the TbtcChain
+// as eligible to join the network. If the operator has a stake delegation or
+// had a stake delegation in the past, it will be recognized.
+func (tc *TbtcChain) IsRecognized(operatorPublicKey *operator.PublicKey) (bool, error) {
+ operatorAddress, err := operatorPublicKeyToChainAddress(operatorPublicKey)
+ if err != nil {
+ return false, fmt.Errorf(
+ "cannot convert from operator key to chain address: [%v]",
+ err,
+ )
+ }
+
+ stakingProvider, err := tc.walletRegistry.OperatorToStakingProvider(
+ operatorAddress,
+ )
+ if err != nil {
+ return false, fmt.Errorf(
+ "failed to map operator [%v] to a staking provider: [%v]",
+ operatorAddress,
+ err,
+ )
+ }
+
+ if (stakingProvider == common.Address{}) {
+ return false, nil
+ }
+
+ // Check if the staking provider has an owner. This check ensures that there
+ // is/was a stake delegation for the given staking provider.
+ _, _, _, hasStakeDelegation, err := tc.baseChain.RolesOf(
+ chain.Address(stakingProvider.Hex()),
+ )
+ if err != nil {
+ return false, fmt.Errorf(
+ "failed to check stake delegation for staking provider [%v]: [%v]",
+ stakingProvider,
+ err,
+ )
+ }
+
+ if !hasStakeDelegation {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+// OperatorToStakingProvider returns the staking provider address for the
+// operator. If the staking provider has not been registered for the
+// operator, the returned address is empty and the boolean flag is set to
+// false. If the staking provider has been registered, the address is not
+// empty and the boolean flag indicates true.
+func (tc *TbtcChain) OperatorToStakingProvider() (chain.Address, bool, error) {
+ stakingProvider, err := tc.walletRegistry.OperatorToStakingProvider(tc.key.Address)
+ if err != nil {
+ return "", false, fmt.Errorf(
+ "failed to map operator [%v] to a staking provider: [%v]",
+ tc.key.Address,
+ err,
+ )
+ }
+
+ if (stakingProvider == common.Address{}) {
+ return "", false, nil
+ }
+
+ return chain.Address(stakingProvider.Hex()), true, nil
+}
+
+// EligibleStake returns the current value of the staking provider's
+// eligible stake. Eligible stake is defined as the currently authorized
+// stake minus the pending authorization decrease. Eligible stake
+// is what is used for operator's weight in the sortition pool.
+// If the authorized stake minus the pending authorization decrease
+// is below the minimum authorization, eligible stake is 0.
+func (tc *TbtcChain) EligibleStake(stakingProvider chain.Address) (*big.Int, error) {
+ eligibleStake, err := tc.walletRegistry.EligibleStake(
+ common.HexToAddress(stakingProvider.String()),
+ )
+ if err != nil {
+ return nil, fmt.Errorf(
+ "failed to get eligible stake for staking provider %s: [%w]",
+ stakingProvider,
+ err,
+ )
+ }
+
+ return eligibleStake, nil
+}
+
+// IsPoolLocked returns true if the sortition pool is locked and no state
+// changes are allowed.
+func (tc *TbtcChain) IsPoolLocked() (bool, error) {
+ return tc.sortitionPool.IsLocked()
+}
+
+// IsOperatorInPool returns true if the operator is registered in
+// the sortition pool.
+func (tc *TbtcChain) IsOperatorInPool() (bool, error) {
+ return tc.walletRegistry.IsOperatorInPool(tc.key.Address)
+}
+
+// IsOperatorUpToDate checks if the operator's authorized stake is in sync
+// with operator's weight in the sortition pool.
+// If the operator's authorized stake is not in sync with sortition pool
+// weight, function returns false.
+// If the operator is not in the sortition pool and their authorized stake
+// is non-zero, function returns false.
+func (tc *TbtcChain) IsOperatorUpToDate() (bool, error) {
+ return tc.walletRegistry.IsOperatorUpToDate(tc.key.Address)
+}
+
+// JoinSortitionPool executes a transaction to have the operator join the
+// sortition pool.
+func (tc *TbtcChain) JoinSortitionPool() error {
+ _, err := tc.walletRegistry.JoinSortitionPool()
+ return err
+}
+
+// UpdateOperatorStatus executes a transaction to update the operator's
+// state in the sortition pool.
+func (tc *TbtcChain) UpdateOperatorStatus() error {
+ _, err := tc.walletRegistry.UpdateOperatorStatus(tc.key.Address)
+ return err
+}
+
+// IsEligibleForRewards checks whether the operator is eligible for rewards
+// or not.
+func (tc *TbtcChain) IsEligibleForRewards() (bool, error) {
+ return tc.sortitionPool.IsEligibleForRewards(tc.key.Address)
+}
+
+// Checks whether the operator is able to restore their eligibility for
+// rewards right away.
+func (tc *TbtcChain) CanRestoreRewardEligibility() (bool, error) {
+ return tc.sortitionPool.CanRestoreRewardEligibility(tc.key.Address)
+}
+
+// Restores reward eligibility for the operator.
+func (tc *TbtcChain) RestoreRewardEligibility() error {
+ _, err := tc.sortitionPool.RestoreRewardEligibility(tc.key.Address)
+ return err
+}
+
+// Returns true if the chaosnet phase is active, false otherwise.
+func (tc *TbtcChain) IsChaosnetActive() (bool, error) {
+ return tc.sortitionPool.IsChaosnetActive()
+}
+
+// Returns true if operator is a beta operator, false otherwise.
+// Chaosnet status does not matter.
+func (tc *TbtcChain) IsBetaOperator() (bool, error) {
+ return tc.sortitionPool.IsBetaOperator(tc.key.Address)
+}
+
+// GetOperatorID returns the ID number of the given operator address. An ID
+// number of 0 means the operator has not been allocated an ID number yet.
+func (tc *TbtcChain) GetOperatorID(
+ operatorAddress chain.Address,
+) (chain.OperatorID, error) {
+ return tc.sortitionPool.GetOperatorID(
+ common.HexToAddress(operatorAddress.String()),
+ )
+}
+
+// SelectGroup returns the group members selected for the current group
+// selection. The function returns an error if the chain's state does not allow
+// for group selection at the moment.
+func (tc *TbtcChain) SelectGroup() (*tbtc.GroupSelectionResult, error) {
+ operatorsIDs, err := tc.walletRegistry.SelectGroup()
+ if err != nil {
+ return nil, fmt.Errorf(
+ "cannot select group in the sortition pool: [%v]",
+ err,
+ )
+ }
+
+ operatorsAddresses, err := tc.sortitionPool.GetIDOperators(operatorsIDs)
+ if err != nil {
+ return nil, fmt.Errorf(
+ "cannot convert operators' IDs to addresses: [%v]",
+ err,
+ )
+ }
+
+ // Should not happen as this is guaranteed by the contract but, just in case.
+ if len(operatorsIDs) != len(operatorsAddresses) {
+ return nil, fmt.Errorf("operators IDs and addresses mismatch")
+ }
+
+ 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
+}
diff --git a/pkg/chain/ethereum/tbtc_wallet.go b/pkg/chain/ethereum/tbtc_wallet.go
new file mode 100644
index 0000000000..008c39c925
--- /dev/null
+++ b/pkg/chain/ethereum/tbtc_wallet.go
@@ -0,0 +1,236 @@
+package ethereum
+
+import (
+ "crypto/ecdsa"
+ "encoding/binary"
+ "fmt"
+ "sort"
+ "time"
+
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/keep-network/keep-core/pkg/bitcoin"
+
+ tbtcabi "github.com/keep-network/keep-core/pkg/chain/ethereum/tbtc/gen/abi"
+ "github.com/keep-network/keep-core/pkg/subscription"
+ "github.com/keep-network/keep-core/pkg/tbtc"
+)
+
+func (tc *TbtcChain) PastNewWalletRegisteredEvents(
+ filter *tbtc.NewWalletRegisteredEventFilter,
+) ([]*tbtc.NewWalletRegisteredEvent, error) {
+ var startBlock uint64
+ var endBlock *uint64
+ var ecdsaWalletID [][32]byte
+ var walletPublicKeyHash [][20]byte
+
+ if filter != nil {
+ startBlock = filter.StartBlock
+ endBlock = filter.EndBlock
+ ecdsaWalletID = filter.EcdsaWalletID
+ walletPublicKeyHash = filter.WalletPublicKeyHash
+ }
+
+ events, err := tc.bridge.PastNewWalletRegisteredEvents(
+ startBlock,
+ endBlock,
+ ecdsaWalletID,
+ walletPublicKeyHash,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0)
+ for _, event := range events {
+ convertedEvent := &tbtc.NewWalletRegisteredEvent{
+ EcdsaWalletID: event.EcdsaWalletID,
+ WalletPublicKeyHash: event.WalletPubKeyHash,
+ BlockNumber: event.Raw.BlockNumber,
+ }
+
+ convertedEvents = append(convertedEvents, convertedEvent)
+ }
+
+ sort.SliceStable(
+ convertedEvents,
+ func(i, j int) bool {
+ return convertedEvents[i].BlockNumber < convertedEvents[j].BlockNumber
+ },
+ )
+
+ return convertedEvents, err
+}
+
+func (tc *TbtcChain) CalculateWalletID(
+ walletPublicKey *ecdsa.PublicKey,
+) ([32]byte, error) {
+ return calculateWalletID(walletPublicKey)
+}
+
+func calculateWalletID(walletPublicKey *ecdsa.PublicKey) ([32]byte, error) {
+ walletPublicKeyBytes, err := convertPubKeyToChainFormat(walletPublicKey)
+ if err != nil {
+ return [32]byte{}, fmt.Errorf(
+ "error while converting wallet public key to chain format: [%v]",
+ err,
+ )
+ }
+
+ return crypto.Keccak256Hash(walletPublicKeyBytes[:]), nil
+}
+
+func (tc *TbtcChain) IsWalletRegistered(EcdsaWalletID [32]byte) (bool, error) {
+ isWalletRegistered, err := tc.walletRegistry.IsWalletRegistered(
+ EcdsaWalletID,
+ )
+ if err != nil {
+ return false, fmt.Errorf(
+ "cannot check if wallet with ECDSA ID [0x%x] is registered: [%v]",
+ EcdsaWalletID,
+ err,
+ )
+ }
+
+ return isWalletRegistered, nil
+}
+
+func (tc *TbtcChain) GetWallet(
+ walletPublicKeyHash [20]byte,
+) (*tbtc.WalletChainData, error) {
+ wallet, err := tc.bridge.Wallets(walletPublicKeyHash)
+ if err != nil {
+ return nil, fmt.Errorf(
+ "cannot get wallet for public key hash [0x%x]: [%v]",
+ walletPublicKeyHash,
+ err,
+ )
+ }
+
+ // Wallet not found.
+ if wallet.CreatedAt == 0 {
+ return nil, fmt.Errorf(
+ "no wallet for public key hash [0x%x]",
+ wallet,
+ )
+ }
+
+ walletState, err := parseWalletState(wallet.State)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse wallet state: [%v]", err)
+ }
+
+ return &tbtc.WalletChainData{
+ EcdsaWalletID: wallet.EcdsaWalletID,
+ MainUtxoHash: wallet.MainUtxoHash,
+ PendingRedemptionsValue: wallet.PendingRedemptionsValue,
+ CreatedAt: time.Unix(int64(wallet.CreatedAt), 0),
+ MovingFundsRequestedAt: time.Unix(int64(wallet.MovingFundsRequestedAt), 0),
+ ClosingStartedAt: time.Unix(int64(wallet.ClosingStartedAt), 0),
+ PendingMovedFundsSweepRequestsCount: wallet.PendingMovedFundsSweepRequestsCount,
+ State: walletState,
+ MovingFundsTargetWalletsCommitmentHash: wallet.MovingFundsTargetWalletsCommitmentHash,
+ }, nil
+}
+
+func (tc *TbtcChain) OnWalletClosed(
+ handler func(event *tbtc.WalletClosedEvent),
+) subscription.EventSubscription {
+ onEvent := func(
+ walletID [32]byte,
+ blockNumber uint64,
+ ) {
+ handler(&tbtc.WalletClosedEvent{
+ WalletID: walletID,
+ BlockNumber: blockNumber,
+ })
+ }
+ return tc.walletRegistry.WalletClosedEvent(nil, nil).OnEvent(onEvent)
+}
+
+func (tc *TbtcChain) ComputeMainUtxoHash(
+ mainUtxo *bitcoin.UnspentTransactionOutput,
+) [32]byte {
+ return computeMainUtxoHash(mainUtxo)
+}
+
+func computeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte {
+ outputIndexBytes := make([]byte, 4)
+ binary.BigEndian.PutUint32(outputIndexBytes, mainUtxo.Outpoint.OutputIndex)
+
+ valueBytes := make([]byte, 8)
+ binary.BigEndian.PutUint64(valueBytes, uint64(mainUtxo.Value))
+
+ mainUtxoHash := crypto.Keccak256Hash(
+ append(
+ append(
+ mainUtxo.Outpoint.TransactionHash[:],
+ outputIndexBytes...,
+ ), valueBytes...,
+ ),
+ )
+
+ return mainUtxoHash
+}
+
+func (tc *TbtcChain) GetWalletParameters() (tbtc.WalletParameters, error) {
+ parameters, err := tc.bridge.WalletParameters()
+ if err != nil {
+ return tbtc.WalletParameters{}, err
+ }
+
+ return tbtc.WalletParameters{
+ CreationPeriod: parameters.WalletCreationPeriod,
+ CreationMinBtcBalance: parameters.WalletCreationMinBtcBalance,
+ CreationMaxBtcBalance: parameters.WalletCreationMaxBtcBalance,
+ ClosureMinBtcBalance: parameters.WalletClosureMinBtcBalance,
+ MaxAge: parameters.WalletMaxAge,
+ MaxBtcTransfer: parameters.WalletMaxBtcTransfer,
+ ClosingPeriod: parameters.WalletClosingPeriod,
+ }, nil
+}
+
+func (tc *TbtcChain) GetLiveWalletsCount() (uint32, error) {
+ return tc.bridge.LiveWalletsCount()
+}
+
+func parseWalletState(value uint8) (tbtc.WalletState, error) {
+ switch value {
+ case 0:
+ return tbtc.StateUnknown, nil
+ case 1:
+ return tbtc.StateLive, nil
+ case 2:
+ return tbtc.StateMovingFunds, nil
+ case 3:
+ return tbtc.StateClosing, nil
+ case 4:
+ return tbtc.StateClosed, nil
+ case 5:
+ return tbtc.StateTerminated, nil
+ default:
+ return 0, fmt.Errorf("unexpected wallet state value: [%v]", value)
+ }
+}
+
+func (tc *TbtcChain) ValidateHeartbeatProposal(
+ walletPublicKeyHash [20]byte,
+ proposal *tbtc.HeartbeatProposal,
+) error {
+ valid, err := tc.walletProposalValidator.ValidateHeartbeatProposal(
+ tbtcabi.WalletProposalValidatorHeartbeatProposal{
+ WalletPubKeyHash: walletPublicKeyHash,
+ Message: proposal.Message[:],
+ },
+ )
+ if err != nil {
+ return fmt.Errorf("validation failed: [%v]", err)
+ }
+
+ // Should never happen because `validateHeartbeatProposal` returns true
+ // or reverts (returns an error) but do the check just in case.
+ if !valid {
+ return fmt.Errorf("unexpected validation result")
+ }
+
+ return nil
+}