diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 0a52004b60..42afc39a82 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -223,6 +223,23 @@ 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, + ) } // NewWalletRegisteredEvent represents a new wallet registered event. diff --git a/pkg/tbtc/chain_test.go b/pkg/tbtc/chain_test.go index be758757cc..b4015fae6d 100644 --- a/pkg/tbtc/chain_test.go +++ b/pkg/tbtc/chain_test.go @@ -1099,6 +1099,23 @@ 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") +} + // 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..900152d22b 100644 --- a/pkg/tbtc/moving_funds.go +++ b/pkg/tbtc/moving_funds.go @@ -313,19 +313,26 @@ func ValidateMovingFundsProposal( ) 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, + ) }, ) 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 +361,78 @@ 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 { + 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, + ) + }, ) 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() + 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 +450,11 @@ func (mfa *movingFundsAction) actionType() WalletActionType { return ActionMovingFunds } +func isWalletPendingMovingFundsTarget() (bool, error) { + // TODO: Implement + 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