diff --git a/pkg/tbtc/moved_funds_sweep.go b/pkg/tbtc/moved_funds_sweep.go index 85f8c8266b..f41e43542c 100644 --- a/pkg/tbtc/moved_funds_sweep.go +++ b/pkg/tbtc/moved_funds_sweep.go @@ -5,6 +5,9 @@ import ( "math/big" "time" + "github.com/keep-network/keep-core/pkg/bitcoin" + "go.uber.org/zap" + "github.com/ipfs/go-log/v2" ) @@ -27,12 +30,37 @@ type MovedFundsSweepRequest struct { } const ( - // movedFundsSweepProposalValidityBlocks determines the moving funds + // movedFundsSweepProposalValidityBlocks determines the moved funds sweep // proposal validity time expressed in blocks. In other words, this is the - // worst-case time for a moving funds during which the wallet is busy and - // cannot take another actions. The value of 600 blocks is roughly 2 hours, - // assuming 12 seconds per block. + // worst-case time for a moved funds sweep during which the wallet is busy + // and cannot take another actions. The value of 600 blocks is roughly + // 2 hours, assuming 12 seconds per block. movedFundsSweepProposalValidityBlocks = 600 + // movedFundsSweepSigningTimeoutSafetyMarginBlocks determines the duration of + // the safety margin that must be preserved between the signing timeout and + // the timeout of the entire moved funds sweep action. This safety margin + // prevents against the case where signing completes late and there is not + // enough time to broadcast the moved funds sweep transaction properly. + // In such a case, wallet signatures may leak and make the wallet subject + // of fraud accusations. Usage of the safety margin ensures there is enough + // time to perform post-signing steps of the moved funds sweep action. + // The value of 300 blocks is roughly 1 hour, assuming 12 seconds per block. + movedFundsSweepSigningTimeoutSafetyMarginBlocks = 300 + // movedFundsSweepBroadcastTimeout determines the time window for moved + // funds sweep transaction broadcast. It is guaranteed that at least + // movedFundsSweepSigningTimeoutSafetyMarginBlocks is preserved for the + // broadcast step. However, the happy path for the broadcast step is usually + // quick and few retries are needed to recover from temporary problems. That + // said, if the broadcast step does not succeed in a tight timeframe, there + // is no point to retry for the entire possible time window. Hence, the + // timeout for broadcast step is set as 25% of the entire time widow + // determined by movedFundsSweepSigningTimeoutSafetyMarginBlocks. + movedFundsSweepBroadcastTimeout = 15 * time.Minute + // movedFundsSweepBroadcastCheckDelay determines the delay that must + // be preserved between transaction broadcast and the check that ensures + // the transaction is known on the Bitcoin chain. This delay is needed + // as spreading the transaction over the Bitcoin network takes time. + movedFundsSweepBroadcastCheckDelay = 1 * time.Minute ) // MovedFundsSweepProposal represents a moved funds sweep proposal issued by a @@ -51,6 +79,69 @@ func (mfsp *MovedFundsSweepProposal) ValidityBlocks() uint64 { return movedFundsSweepProposalValidityBlocks } +type movedFundsSweepAction struct { + logger *zap.SugaredLogger + chain Chain + btcChain bitcoin.Chain + + movedFundsSweepWallet wallet + transactionExecutor *walletTransactionExecutor + + proposal *MovedFundsSweepProposal + proposalProcessingStartBlock uint64 + proposalExpiryBlock uint64 + + signingTimeoutSafetyMarginBlocks uint64 + broadcastTimeout time.Duration + broadcastCheckDelay time.Duration +} + +func newMovedFundsSweepAction( + logger *zap.SugaredLogger, + chain Chain, + btcChain bitcoin.Chain, + movedFundsSweepWallet wallet, + signingExecutor walletSigningExecutor, + proposal *MovedFundsSweepProposal, + proposalProcessingStartBlock uint64, + proposalExpiryBlock uint64, + waitForBlockFn waitForBlockFn, +) *movedFundsSweepAction { + transactionExecutor := newWalletTransactionExecutor( + btcChain, + movedFundsSweepWallet, + signingExecutor, + waitForBlockFn, + ) + + return &movedFundsSweepAction{ + logger: logger, + chain: chain, + btcChain: btcChain, + movedFundsSweepWallet: movedFundsSweepWallet, + transactionExecutor: transactionExecutor, + proposal: proposal, + proposalProcessingStartBlock: proposalProcessingStartBlock, + proposalExpiryBlock: proposalExpiryBlock, + signingTimeoutSafetyMarginBlocks: movedFundsSweepSigningTimeoutSafetyMarginBlocks, + broadcastTimeout: movedFundsSweepBroadcastTimeout, + broadcastCheckDelay: movedFundsSweepBroadcastCheckDelay, + } +} + +func (mfsa *movedFundsSweepAction) execute() error { + // TODO: Implement + return nil +} + +func (mfsa *movedFundsSweepAction) wallet() wallet { + return mfsa.movedFundsSweepWallet +} + +func (mfsa *movedFundsSweepAction) actionType() WalletActionType { + return ActionMovedFundsSweep +} + // ValidateMovedFundsSweepProposal checks the moved funds sweep proposal with // on-chain validation rules. func ValidateMovedFundsSweepProposal( diff --git a/pkg/tbtc/node.go b/pkg/tbtc/node.go index 10e0122121..1b06c5dc2d 100644 --- a/pkg/tbtc/node.go +++ b/pkg/tbtc/node.go @@ -684,7 +684,64 @@ func (n *node) handleMovedFundsSweepProposal( startBlock uint64, expiryBlock uint64, ) { - // TODO: Implement + walletPublicKeyBytes, err := marshalPublicKey(wallet.publicKey) + if err != nil { + logger.Errorf("cannot marshal wallet public key: [%v]", err) + return + } + + signingExecutor, ok, err := n.getSigningExecutor(wallet.publicKey) + if err != nil { + logger.Errorf("cannot get signing executor: [%v]", err) + return + } + // This check is actually redundant. We know the node controls some + // wallet signers as we just got the wallet from the registry using their + // public key hash. However, we are doing it just in case. The API + // contract of getSigningExecutor may change one day. + if !ok { + logger.Infof( + "node does not control signers of wallet PKH [0x%x]; "+ + "ignoring the received moved funds sweep proposal", + walletPublicKeyBytes, + ) + return + } + + logger.Infof( + "starting orchestration of the moved funds sweep action for wallet "+ + "[0x%x]; 20-byte public key hash of that wallet is [0x%x]", + walletPublicKeyBytes, + bitcoin.PublicKeyHash(wallet.publicKey), + ) + + walletActionLogger := logger.With( + zap.String("wallet", fmt.Sprintf("0x%x", walletPublicKeyBytes)), + zap.String("action", ActionMovedFundsSweep.String()), + zap.Uint64("startBlock", startBlock), + zap.Uint64("expiryBlock", expiryBlock), + ) + walletActionLogger.Infof("dispatching wallet action") + + action := newMovedFundsSweepAction( + walletActionLogger, + n.chain, + n.btcChain, + wallet, + signingExecutor, + proposal, + startBlock, + expiryBlock, + n.waitForBlockHeight, + ) + + err = n.walletDispatcher.dispatch(action) + if err != nil { + walletActionLogger.Errorf("cannot dispatch wallet action: [%v]", err) + return + } + + walletActionLogger.Infof("wallet action dispatched successfully") } // coordinationLayerSettings represents settings for the coordination layer.