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)