From ed9c4ae7289e1609150762c1733298f2268be1ec Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Thu, 22 Feb 2024 18:27:02 +0100 Subject: [PATCH] Check veto processing delay during redemption proposal generation The optimistic redemption upgrade introduces a veto mechanism that enforces a processing delay on each redemption request. The exact delay value depends on the number of objections raised against the given redemption request. The `WalletProposalValidator` contract has been modified to include validation of that delay factor. Here we introduce the same for the redemption proposal generator. This ensures the generator issues proposals that conform the on-chain validation rules and coordination windows are not being wasted. See: https://github.com/keep-network/tbtc-v2/pull/788 --- pkg/chain/ethereum/tbtc.go | 52 ++++++++++++++++ pkg/tbtcpg/chain.go | 6 ++ pkg/tbtcpg/chain_test.go | 32 ++++++++++ pkg/tbtcpg/internal/test/marshaling.go | 2 + pkg/tbtcpg/internal/test/tbtcpgtest.go | 1 + .../find_pending_redemptions_scenario_3.json | 60 +++++++++++++++++++ pkg/tbtcpg/redemptions.go | 50 ++++++++++++++-- pkg/tbtcpg/redemptions_test.go | 7 +++ 8 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 pkg/tbtcpg/internal/test/testdata/find_pending_redemptions_scenario_3.json diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index f64f2480be..6082b8a703 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -49,6 +49,7 @@ type TbtcChain struct { walletRegistry *ecdsacontract.WalletRegistry sortitionPool *ecdsacontract.EcdsaSortitionPool walletProposalValidator *tbtccontract.WalletProposalValidator + redemptionWatchtower *tbtccontract.RedemptionWatchtower } // NewTbtcChain construct a new instance of the TBTC-specific Ethereum @@ -194,6 +195,35 @@ func newTbtcChain( ) } + redemptionWatchtowerAddress, err := bridge.GetRedemptionWatchtower() + if err != nil { + return nil, fmt.Errorf( + "failed to get RedemptionWatchtower address from Bridge: [%v]", + err, + ) + } + + var redemptionWatchtower *tbtccontract.RedemptionWatchtower + if redemptionWatchtowerAddress != [20]byte{} { + redemptionWatchtower, err = + tbtccontract.NewRedemptionWatchtower( + redemptionWatchtowerAddress, + baseChain.chainID, + baseChain.key, + baseChain.client, + baseChain.nonceManager, + baseChain.miningWaiter, + baseChain.blockCounter, + baseChain.transactionMutex, + ) + if err != nil { + return nil, fmt.Errorf( + "failed to attach to RedemptionWatchtower contract: [%v]", + err, + ) + } + } + return &TbtcChain{ baseChain: baseChain, bridge: bridge, @@ -201,6 +231,7 @@ func newTbtcChain( walletRegistry: walletRegistry, sortitionPool: sortitionPool, walletProposalValidator: walletProposalValidator, + redemptionWatchtower: redemptionWatchtower, }, nil } @@ -1898,3 +1929,24 @@ func (tc *TbtcChain) ValidateMovingFundsProposal( 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 +} diff --git a/pkg/tbtcpg/chain.go b/pkg/tbtcpg/chain.go index f9887973be..9bbf9f97b6 100644 --- a/pkg/tbtcpg/chain.go +++ b/pkg/tbtcpg/chain.go @@ -178,4 +178,10 @@ type Chain interface { // Computes the moving funds commitment hash from the provided public key // hashes of target wallets. ComputeMovingFundsCommitmentHash(targetWallets [][20]byte) [32]byte + + // GetRedemptionDelay returns the processing delay for the given redemption. + GetRedemptionDelay( + walletPublicKeyHash [20]byte, + redeemerOutputScript bitcoin.Script, + ) (time.Duration, error) } diff --git a/pkg/tbtcpg/chain_test.go b/pkg/tbtcpg/chain_test.go index 54cb8b5ff9..5c50cde131 100644 --- a/pkg/tbtcpg/chain_test.go +++ b/pkg/tbtcpg/chain_test.go @@ -90,6 +90,7 @@ type LocalChain struct { movingFundsProposalValidations map[[32]byte]bool movingFundsCommitmentSubmissions []*movingFundsCommitmentSubmission operatorIDs map[chain.Address]uint32 + redemptionDelays map[[32]byte]time.Duration } func NewLocalChain() *LocalChain { @@ -107,6 +108,7 @@ func NewLocalChain() *LocalChain { movingFundsProposalValidations: make(map[[32]byte]bool), movingFundsCommitmentSubmissions: make([]*movingFundsCommitmentSubmission, 0), operatorIDs: make(map[chain.Address]uint32), + redemptionDelays: make(map[[32]byte]time.Duration), } } @@ -1087,6 +1089,36 @@ func (lc *LocalChain) GetMovingFundsSubmissions() []*movingFundsCommitmentSubmis return lc.movingFundsCommitmentSubmissions } +func (lc *LocalChain) GetRedemptionDelay( + walletPublicKeyHash [20]byte, + redeemerOutputScript bitcoin.Script, +) (time.Duration, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key := buildRedemptionRequestKey(walletPublicKeyHash, redeemerOutputScript) + + delay, ok := lc.redemptionDelays[key] + if !ok { + return 0, fmt.Errorf("redemption delay not found") + } + + return delay, nil +} + +func (lc *LocalChain) SetRedemptionDelay( + walletPublicKeyHash [20]byte, + redeemerOutputScript bitcoin.Script, + delay time.Duration, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key := buildRedemptionRequestKey(walletPublicKeyHash, redeemerOutputScript) + + lc.redemptionDelays[key] = delay +} + type MockBlockCounter struct { mutex sync.Mutex currentBlock uint64 diff --git a/pkg/tbtcpg/internal/test/marshaling.go b/pkg/tbtcpg/internal/test/marshaling.go index ed919ec615..5b859300f4 100644 --- a/pkg/tbtcpg/internal/test/marshaling.go +++ b/pkg/tbtcpg/internal/test/marshaling.go @@ -276,6 +276,7 @@ func (fprts *FindPendingRedemptionsTestScenario) UnmarshalJSON(data []byte) erro RedeemerOutputScript string RequestedAmount uint64 Age int64 + Delay int64 } ExpectedRedeemersOutputScripts []string } @@ -324,6 +325,7 @@ func (fprts *FindPendingRedemptionsTestScenario) UnmarshalJSON(data []byte) erro RequestedAmount: pr.RequestedAmount, RequestedAt: requestedAt, RequestBlock: requestBlock, + Delay: time.Duration(pr.Delay) * time.Second, }, ) } diff --git a/pkg/tbtcpg/internal/test/tbtcpgtest.go b/pkg/tbtcpg/internal/test/tbtcpgtest.go index 3ed3209a75..67183670a7 100644 --- a/pkg/tbtcpg/internal/test/tbtcpgtest.go +++ b/pkg/tbtcpg/internal/test/tbtcpgtest.go @@ -100,6 +100,7 @@ type RedemptionRequest struct { RequestedAmount uint64 RequestedAt time.Time RequestBlock uint64 + Delay time.Duration } // FindPendingRedemptionsTestScenario represents a test scenario of finding diff --git a/pkg/tbtcpg/internal/test/testdata/find_pending_redemptions_scenario_3.json b/pkg/tbtcpg/internal/test/testdata/find_pending_redemptions_scenario_3.json new file mode 100644 index 0000000000..05d33e3aa3 --- /dev/null +++ b/pkg/tbtcpg/internal/test/testdata/find_pending_redemptions_scenario_3.json @@ -0,0 +1,60 @@ +{ + "Title": "pending redemptions with different processing delays exist", + "ChainParameters":{ + "AverageBlockTime": 10, + "CurrentBlock": 100000, + "RequestTimeout": 86400, + "RequestMinAge": 3600 + }, + "MaxNumberOfRequests": 10, + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "PendingRedemptions": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000001", + "RequestedAmount": 1000000000, + "Age": 3000, + "Delay": 0 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000002", + "RequestedAmount": 2000000000, + "Age": 4000, + "Delay": 0 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000003", + "RequestedAmount": 3000000000, + "Age": 4000, + "Delay": 7200 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000004", + "RequestedAmount": 4000000000, + "Age": 8000, + "Delay": 7200 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000005", + "RequestedAmount": 5000000000, + "Age": 8000, + "Delay": 14400 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000006", + "RequestedAmount": 6000000000, + "Age": 15000, + "Delay": 14400 + } + ], + "ExpectedRedeemersOutputScripts": [ + "0x00140000000000000000000000000000000000000006", + "0x00140000000000000000000000000000000000000004", + "0x00140000000000000000000000000000000000000002" + ] +} diff --git a/pkg/tbtcpg/redemptions.go b/pkg/tbtcpg/redemptions.go index 1401a13ec7..c9c01a5856 100644 --- a/pkg/tbtcpg/redemptions.go +++ b/pkg/tbtcpg/redemptions.go @@ -366,15 +366,42 @@ redemptionRequestedLoop: }, ) + // Capture time now for computations. + timeNow := time.Now() + // Only redemption requests in range: - // [now - requestTimeout, now - requestMinAge] + // [now - requestTimeout, now - minAge] // should be taken into consideration. - redemptionRequestsRangeStartTimestamp := time.Now().Add( + redemptionRequestsRangeStartTimestamp := timeNow.Add( -time.Duration(requestTimeout) * time.Second, ) - redemptionRequestsRangeEndTimestamp := time.Now().Add( - -time.Duration(requestMinAge) * time.Second, - ) + redemptionRequestsRangeEndTimestampFn := func( + redemption *RedemptionRequest, + ) (time.Time, error) { + delay, err := chain.GetRedemptionDelay( + redemption.WalletPublicKeyHash, + redemption.RedeemerOutputScript, + ) + if err != nil { + return time.Time{}, fmt.Errorf( + "failed to get redemption delay: [%w]", + err, + ) + } + + minAge := time.Duration(requestMinAge) * time.Second + if delay > minAge { + minAge = delay + } + + fnLogger.Infof( + "minimum age for redemption request [%s] is [%v]", + redemption.RedemptionKey, + minAge, + ) + + return timeNow.Add(-minAge), nil + } result := make([]*RedemptionRequest, 0, resultSliceCapacity) for _, pendingRedemption := range pendingRedemptions { @@ -391,8 +418,19 @@ redemptionRequestedLoop: continue } + rangeEndTimestamp, err := redemptionRequestsRangeEndTimestampFn( + pendingRedemption, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot get minimum age for redemption request [%s]: [%w]", + pendingRedemption.RedemptionKey, + err, + ) + } + // Check if enough time elapsed since the redemption request. - if pendingRedemption.RequestedAt.After(redemptionRequestsRangeEndTimestamp) { + if pendingRedemption.RequestedAt.After(rangeEndTimestamp) { fnLogger.Infof( "redemption request [%s] is not old enough", pendingRedemption.RedemptionKey, diff --git a/pkg/tbtcpg/redemptions_test.go b/pkg/tbtcpg/redemptions_test.go index c9dff57417..8f61e2f94b 100644 --- a/pkg/tbtcpg/redemptions_test.go +++ b/pkg/tbtcpg/redemptions_test.go @@ -106,6 +106,13 @@ func TestRedemptionAction_FindPendingRedemptions(t *testing.T) { RequestedAt: pendingRedemption.RequestedAt, }, ) + + // Record the redemption processing delay. + tbtcChain.SetRedemptionDelay( + pendingRedemption.WalletPublicKeyHash, + pendingRedemption.RedeemerOutputScript, + pendingRedemption.Delay, + ) } task := tbtcpg.NewRedemptionTask(tbtcChain, nil)