From ec00a8912fa7acfd2d90f9bb464f242efbf4ae25 Mon Sep 17 00:00:00 2001 From: Tomasz Slabon Date: Tue, 30 Apr 2024 19:15:55 +0200 Subject: [PATCH] Adjusted safety margin calculations when moving funds --- pkg/tbtc/chain.go | 26 +++++ pkg/tbtc/chain_test.go | 23 +++++ pkg/tbtc/moving_funds.go | 207 +++++++++++++++++++++++++++++++++++-- pkg/tbtcpg/chain.go | 17 --- pkg/tbtcpg/moving_funds.go | 2 +- 5 files changed, 247 insertions(+), 28 deletions(-) diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 0a52004b60..7563b76b11 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -223,6 +223,32 @@ type BridgeChain interface { movingFundsTxHash bitcoin.Hash, movingFundsTxOutpointIndex uint32, ) (*MovedFundsSweepRequest, bool, error) + + // GetMovingFundsParameters gets the current value of parameters relevant + // for the moving funds process. + GetMovingFundsParameters() ( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, + err error, + ) + + // PastMovingFundsCommitmentSubmittedEvents fetches past moving funds + // commitment submitted events according to the provided filter or + // unfiltered if the filter is nil. Returned events are sorted by the block + // number in the ascending order, i.e. the latest event is at the end of the + // slice. + PastMovingFundsCommitmentSubmittedEvents( + filter *MovingFundsCommitmentSubmittedEventFilter, + ) ([]*MovingFundsCommitmentSubmittedEvent, error) } // NewWalletRegisteredEvent represents a new wallet registered event. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index be758757cc..6e93b92a37 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -1099,6 +1099,29 @@ func buildMovedFundsSweepProposalValidationKey( return sha256.Sum256(buffer.Bytes()), nil } +func (lc *localChain) GetMovingFundsParameters() ( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, + err error, +) { + panic("unsupported") +} + +func (lc *localChain) PastMovingFundsCommitmentSubmittedEvents( + filter *MovingFundsCommitmentSubmittedEventFilter, +) ([]*MovingFundsCommitmentSubmittedEvent, error) { + panic("unsupported") +} + // Connect sets up the local chain. func Connect(blockTime ...time.Duration) *localChain { operatorPrivateKey, _, err := operator.GenerateKeyPair(local_v1.DefaultCurve) diff --git a/pkg/tbtc/moving_funds.go b/pkg/tbtc/moving_funds.go index a030399414..b1d4b61639 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -9,6 +9,7 @@ import ( "github.com/keep-network/keep-common/pkg/chain/ethereum" "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/chain" "go.uber.org/zap" ) @@ -53,6 +54,11 @@ const ( movingFundsCommitmentConfirmationBlocks = 32 ) +// MovingFundsCommitmentLookBackBlocks is the look-back period in blocks used +// when searching for submitted moving funds commitment events. It's equal to +// 30 days assuming 12 seconds per block. +const MovingFundsCommitmentLookBackBlocks = uint64(216000) + // MovingFundsProposal represents a moving funds proposal issued by a wallet's // coordination leader. type MovingFundsProposal struct { @@ -312,20 +318,33 @@ func ValidateMovingFundsProposal( proposal *MovingFundsProposal, ) error + BlockCounter() (chain.BlockCounter, error) + GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error) + + GetMovingFundsParameters() ( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, + err error, + ) + + PastMovingFundsCommitmentSubmittedEvents( + filter *MovingFundsCommitmentSubmittedEventFilter, + ) ([]*MovingFundsCommitmentSubmittedEvent, error) }, ) error { validateProposalLogger.Infof("calling chain for proposal validation") - walletChainData, err := chain.GetWallet(walletPublicKeyHash) - if err != nil { - return fmt.Errorf( - "cannot get wallet's chain data: [%w]", - err, - ) - } - - err = ValidateMovingFundsSafetyMargin(walletChainData) + err := ValidateMovingFundsSafetyMargin(walletPublicKeyHash, chain) if err != nil { return fmt.Errorf("moving funds proposal is invalid: [%v]", err) } @@ -354,10 +373,87 @@ func ValidateMovingFundsProposal( // deposits so, it makes sense to preserve a safety margin before moving // funds to give the last minute deposits a chance to become eligible for // deposit sweep. +// +// Similarly, wallets that just entered the MovingFunds state may have become +// target wallets for another moving funds wallets. It makes sense to preserve +// a safety margin to allow the wallet to merge the moved funds from another +// wallets. In this case a longer safety margin should be used. func ValidateMovingFundsSafetyMargin( - walletChainData *WalletChainData, + walletPublicKeyHash [20]byte, + chain interface { + BlockCounter() (chain.BlockCounter, error) + + GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error) + + GetMovingFundsParameters() ( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, + err error, + ) + + PastMovingFundsCommitmentSubmittedEvents( + filter *MovingFundsCommitmentSubmittedEventFilter, + ) ([]*MovingFundsCommitmentSubmittedEvent, error) + }, ) error { + // In most cases the safety margin of 24 hours should be enough. It will + // allow the wallet to sweep the last deposits that were made before the + // wallet entered the moving funds state. safetyMargin := time.Duration(24) * time.Hour + + // It is possible that our wallet is the target wallet in another pending + // moving funds procedure. If this is the case we must apply a longer + // 14-day safety margin. This will ensure the funds moved from another + // wallet can be merged with our wallet's main UTXO before moving funds. + isMovingFundsTarget, err := isWalletPendingMovingFundsTarget( + walletPublicKeyHash, + chain, + ) + if err != nil { + return fmt.Errorf( + "cannot check if wallet is pending moving funds target: [%w]", + err, + ) + } + + if isMovingFundsTarget { + safetyMargin = time.Duration(24) * 14 * time.Hour + } + + // As the moving funds procedure is time constrained, we must ensure the + // safety margin does not exceed half of the moving funds timeout parameter. + // This should give the wallet enough time to complete moving funds. + _, _, _, movingFundsTimeout, _, _, _, _, _, _, _, err := + chain.GetMovingFundsParameters() + if err != nil { + return fmt.Errorf("cannot get moving funds parameters: [%w]", err) + } + + maxAllowedSafetyMargin := time.Duration( + float64(movingFundsTimeout) * 0.5 * float64(time.Second), + ) + + if safetyMargin > maxAllowedSafetyMargin { + safetyMargin = maxAllowedSafetyMargin + } + + walletChainData, err := chain.GetWallet(walletPublicKeyHash) + if err != nil { + return fmt.Errorf( + "cannot get wallet's chain data: [%w]", + err, + ) + } + safetyMarginExpiresAt := walletChainData.MovingFundsRequestedAt.Add(safetyMargin) if time.Now().Before(safetyMarginExpiresAt) { @@ -375,6 +471,97 @@ func (mfa *movingFundsAction) actionType() WalletActionType { return ActionMovingFunds } +func isWalletPendingMovingFundsTarget( + walletPublicKeyHash [20]byte, + + chain interface { + BlockCounter() (chain.BlockCounter, error) + + GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error) + + GetMovingFundsParameters() ( + txMaxTotalFee uint64, + dustThreshold uint64, + timeoutResetDelay uint32, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + commitmentGasOffset uint16, + sweepTxMaxTotalFee uint64, + sweepTimeout uint32, + sweepTimeoutSlashingAmount *big.Int, + sweepTimeoutNotifierRewardMultiplier uint32, + err error, + ) + + PastMovingFundsCommitmentSubmittedEvents( + filter *MovingFundsCommitmentSubmittedEventFilter, + ) ([]*MovingFundsCommitmentSubmittedEvent, error) + }, +) (bool, error) { + blockCounter, err := chain.BlockCounter() + if err != nil { + return false, fmt.Errorf("failed to get block counter: [%w]", err) + } + + currentBlockNumber, err := blockCounter.CurrentBlock() + if err != nil { + return false, fmt.Errorf( + "failed to get current block number: [%w]", + err, + ) + } + + filterStartBlock := uint64(0) + if currentBlockNumber > MovingFundsCommitmentLookBackBlocks { + filterStartBlock = currentBlockNumber - MovingFundsCommitmentLookBackBlocks + } + + // Get all the recent moving funds commitment submitted events. + filter := &MovingFundsCommitmentSubmittedEventFilter{ + StartBlock: filterStartBlock, + } + + events, err := chain.PastMovingFundsCommitmentSubmittedEvents(filter) + if err != nil { + return false, fmt.Errorf( + "failed to get past moving funds commitment submitted events: [%w]", + err, + ) + } + + isWalletTarget := func(event *MovingFundsCommitmentSubmittedEvent) bool { + for _, targetWallet := range event.TargetWallets { + if walletPublicKeyHash == targetWallet { + return true + } + } + return false + } + + for _, event := range events { + if !isWalletTarget(event) { + continue + } + + // Our wallet is on the list of target wallets. If the state is moving + // funds, there is probably moving funds to our wallet in the process. + walletChainData, err := chain.GetWallet(walletPublicKeyHash) + if err != nil { + return false, fmt.Errorf( + "cannot get wallet's chain data: [%w]", + err, + ) + } + + if walletChainData.State == StateMovingFunds { + return true, nil + } + } + + return false, nil +} + func assembleMovingFundsTransaction( bitcoinChain bitcoin.Chain, walletMainUtxo *bitcoin.UnspentTransactionOutput, diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index 6a85f121ab..01e519c462 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -131,23 +131,6 @@ type Chain interface { proposal *tbtc.HeartbeatProposal, ) error - // GetMovingFundsParameters gets the current value of parameters relevant - // for the moving funds process. - GetMovingFundsParameters() ( - txMaxTotalFee uint64, - dustThreshold uint64, - timeoutResetDelay uint32, - timeout uint32, - timeoutSlashingAmount *big.Int, - timeoutNotifierRewardMultiplier uint32, - commitmentGasOffset uint16, - sweepTxMaxTotalFee uint64, - sweepTimeout uint32, - sweepTimeoutSlashingAmount *big.Int, - sweepTimeoutNotifierRewardMultiplier uint32, - err error, - ) - // PastMovingFundsCommitmentSubmittedEvents fetches past moving funds // commitment submitted events according to the provided filter or // unfiltered if the filter is nil. Returned events are sorted by the block diff --git a/pkg/tbtcpg/moving_funds.go b/pkg/tbtcpg/moving_funds.go index 20cc8d7ba4..22a842abc8 100644 --- a/pkg/tbtcpg/moving_funds.go +++ b/pkg/tbtcpg/moving_funds.go @@ -95,7 +95,7 @@ func (mft *MovingFundsTask) Run(request *tbtc.CoordinationProposalRequest) ( // Check the safety margin for moving funds early. This will prevent // commitment submission if the wallet is not safe to move funds. - err = tbtc.ValidateMovingFundsSafetyMargin(walletChainData) + err = tbtc.ValidateMovingFundsSafetyMargin(walletPublicKeyHash, mft.chain) if err != nil { taskLogger.Infof("source wallet moving funds safety margin validation failed: [%v]", err) return nil, false, nil