diff --git a/pkg/maintainer/wallet/chain_test.go b/pkg/maintainer/wallet/chain_test.go index f4ea08f57d..271a25a9d8 100644 --- a/pkg/maintainer/wallet/chain_test.go +++ b/pkg/maintainer/wallet/chain_test.go @@ -2,6 +2,7 @@ package wallet import ( "bytes" + "context" "crypto/sha256" "encoding/binary" "encoding/hex" @@ -27,6 +28,16 @@ type depositParameters = struct { revealAheadPeriod uint32 } +type redemptionParameters = struct { + dustThreshold uint64 + treasuryFeeDivisor uint64 + txMaxFee uint64 + txMaxTotalFee uint64 + timeout uint32 + timeoutSlashingAmount *big.Int + timeoutNotifierRewardMultiplier uint32 +} + type LocalChain struct { mutex sync.Mutex @@ -37,6 +48,14 @@ type LocalChain struct { depositSweepProposalValidations map[[32]byte]bool depositSweepProposals []*tbtc.DepositSweepProposal walletLocks map[[20]byte]*walletLock + redemptionParameters redemptionParameters + redemptionRequestMinAge uint32 + blockCounter chain.BlockCounter + pastRedemptionRequestedEvents map[[32]byte][]*tbtc.RedemptionRequestedEvent + averageBlockTime time.Duration + pendingRedemptionRequests map[[32]byte]*tbtc.RedemptionRequest + redemptionProposals []*tbtc.RedemptionProposal + redemptionProposalValidations map[[32]byte]bool } func NewLocalChain() *LocalChain { @@ -46,6 +65,9 @@ func NewLocalChain() *LocalChain { pastNewWalletRegisteredEvents: make(map[[32]byte][]*tbtc.NewWalletRegisteredEvent), depositSweepProposalValidations: make(map[[32]byte]bool), walletLocks: make(map[[20]byte]*walletLock), + pastRedemptionRequestedEvents: make(map[[32]byte][]*tbtc.RedemptionRequestedEvent), + pendingRedemptionRequests: make(map[[32]byte]*tbtc.RedemptionRequest), + redemptionProposalValidations: make(map[[32]byte]bool), } } @@ -56,6 +78,13 @@ func (lc *LocalChain) DepositSweepProposals() []*tbtc.DepositSweepProposal { return lc.depositSweepProposals } +func (lc *LocalChain) RedemptionProposals() []*tbtc.RedemptionProposal { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.redemptionProposals +} + func (lc *LocalChain) PastDepositRevealedEvents( filter *tbtc.DepositRevealedEventFilter, ) ([]*tbtc.DepositRevealedEvent, error) { @@ -236,7 +265,75 @@ func buildPastNewWalletRegisteredEventsKey( func (lc *LocalChain) PastRedemptionRequestedEvents( filter *tbtc.RedemptionRequestedEventFilter, ) ([]*tbtc.RedemptionRequestedEvent, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastRedemptionRequestedEventsKey(filter) + if err != nil { + return nil, err + } + + events, ok := lc.pastRedemptionRequestedEvents[eventsKey] + if !ok { + return nil, fmt.Errorf("no events for given filter") + } + + return events, nil +} + +func (lc *LocalChain) AddPastRedemptionRequestedEvent( + filter *tbtc.RedemptionRequestedEventFilter, + event *tbtc.RedemptionRequestedEvent, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastRedemptionRequestedEventsKey(filter) + if err != nil { + return err + } + + lc.pastRedemptionRequestedEvents[eventsKey] = append( + lc.pastRedemptionRequestedEvents[eventsKey], + event, + ) + + return nil +} + +func buildPastRedemptionRequestedEventsKey( + filter *tbtc.RedemptionRequestedEventFilter, +) ([32]byte, error) { + if filter == nil { + return [32]byte{}, nil + } + + var buffer bytes.Buffer + + startBlock := make([]byte, 8) + binary.BigEndian.PutUint64(startBlock, filter.StartBlock) + buffer.Write(startBlock) + + if filter.EndBlock != nil { + endBlock := make([]byte, 8) + binary.BigEndian.PutUint64(startBlock, *filter.EndBlock) + buffer.Write(endBlock) + } + + for _, walletPublicKeyHash := range filter.WalletPublicKeyHash { + buffer.Write(walletPublicKeyHash[:]) + } + + for _, redeemer := range filter.Redeemer { + redeemerHex, err := hex.DecodeString(redeemer.String()) + if err != nil { + return [32]byte{}, err + } + + buffer.Write(redeemerHex) + } + + return sha256.Sum256(buffer.Bytes()), nil } func (lc *LocalChain) BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int { @@ -259,7 +356,19 @@ func (lc *LocalChain) BuildRedemptionKey( walletPublicKeyHash [20]byte, redeemerOutputScript bitcoin.Script, ) (*big.Int, error) { - panic("unsupported") + redemptionKeyBytes := buildRedemptionRequestKey( + walletPublicKeyHash, + redeemerOutputScript, + ) + + return new(big.Int).SetBytes(redemptionKeyBytes[:]), nil +} + +func buildRedemptionRequestKey( + walletPublicKeyHash [20]byte, + redeemerOutputScript bitcoin.Script, +) [32]byte { + return sha256.Sum256(append(walletPublicKeyHash[:], redeemerOutputScript...)) } func (lc *LocalChain) GetDepositParameters() ( @@ -283,7 +392,32 @@ func (lc *LocalChain) GetPendingRedemptionRequest( walletPublicKeyHash [20]byte, redeemerOutputScript bitcoin.Script, ) (*tbtc.RedemptionRequest, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + requestKey := buildRedemptionRequestKey(walletPublicKeyHash, redeemerOutputScript) + + request, ok := lc.pendingRedemptionRequests[requestKey] + if !ok { + return nil, fmt.Errorf("not request for given key") + } + + return request, nil +} + +func (lc *LocalChain) SetPendingRedemptionRequest( + walletPublicKeyHash [20]byte, + request *tbtc.RedemptionRequest, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + requestKey := buildRedemptionRequestKey( + walletPublicKeyHash, + request.RedeemerOutputScript, + ) + + lc.pendingRedemptionRequests[requestKey] = request } func (lc *LocalChain) SetDepositParameters( @@ -313,7 +447,40 @@ func (lc *LocalChain) GetRedemptionParameters() ( timeoutNotifierRewardMultiplier uint32, err error, ) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.redemptionParameters.dustThreshold, + lc.redemptionParameters.treasuryFeeDivisor, + lc.redemptionParameters.txMaxFee, + lc.redemptionParameters.txMaxTotalFee, + lc.redemptionParameters.timeout, + lc.redemptionParameters.timeoutSlashingAmount, + lc.redemptionParameters.timeoutNotifierRewardMultiplier, + nil +} + +func (lc *LocalChain) SetRedemptionParameters( + dustThreshold uint64, + treasuryFeeDivisor uint64, + txMaxFee uint64, + txMaxTotalFee uint64, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.redemptionParameters = redemptionParameters{ + dustThreshold: dustThreshold, + treasuryFeeDivisor: treasuryFeeDivisor, + txMaxFee: txMaxFee, + txMaxTotalFee: txMaxTotalFee, + timeout: timeout, + timeoutSlashingAmount: timeoutSlashingAmount, + timeoutNotifierRewardMultiplier: timeoutNotifierRewardMultiplier, + } } func (lc *LocalChain) ValidateDepositSweepProposal( @@ -398,13 +565,68 @@ func (lc *LocalChain) SubmitDepositSweepProposalWithReimbursement( func (lc *LocalChain) SubmitRedemptionProposalWithReimbursement( proposal *tbtc.RedemptionProposal, ) error { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.redemptionProposals = append(lc.redemptionProposals, proposal) + + return nil } func (lc *LocalChain) ValidateRedemptionProposal( proposal *tbtc.RedemptionProposal, ) error { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key, err := buildRedemptionProposalValidationKey(proposal) + if err != nil { + return err + } + + result, ok := lc.redemptionProposalValidations[key] + if !ok { + return fmt.Errorf("validation result unknown") + } + + if !result { + return fmt.Errorf("validation failed") + } + + return nil +} + +func (lc *LocalChain) SetRedemptionProposalValidationResult( + proposal *tbtc.RedemptionProposal, + result bool, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + key, err := buildRedemptionProposalValidationKey(proposal) + if err != nil { + return err + } + + lc.redemptionProposalValidations[key] = result + + return nil +} + +func buildRedemptionProposalValidationKey( + proposal *tbtc.RedemptionProposal, +) ([32]byte, error) { + var buffer bytes.Buffer + + buffer.Write(proposal.WalletPublicKeyHash[:]) + + for _, script := range proposal.RedeemersOutputScripts { + buffer.Write(script) + } + + buffer.Write(proposal.RedemptionTxFee.Bytes()) + + return sha256.Sum256(buffer.Bytes()), nil } func (lc *LocalChain) GetRedemptionMaxSize() (uint16, error) { @@ -412,7 +634,17 @@ func (lc *LocalChain) GetRedemptionMaxSize() (uint16, error) { } func (lc *LocalChain) GetRedemptionRequestMinAge() (uint32, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.redemptionRequestMinAge, nil +} + +func (lc *LocalChain) SetRedemptionRequestMinAge(redemptionRequestMinAge uint32) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.redemptionRequestMinAge = redemptionRequestMinAge } func (lc *LocalChain) GetDepositSweepMaxSize() (uint16, error) { @@ -460,9 +692,67 @@ func (lc *LocalChain) ResetWalletLock( } func (lc *LocalChain) BlockCounter() (chain.BlockCounter, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.blockCounter, nil +} + +func (lc *LocalChain) SetBlockCounter(blockCounter chain.BlockCounter) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.blockCounter = blockCounter } func (lc *LocalChain) AverageBlockTime() time.Duration { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.averageBlockTime +} + +func (lc *LocalChain) SetAverageBlockTime(averageBlockTime time.Duration) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.averageBlockTime = averageBlockTime +} + +type MockBlockCounter struct { + mutex sync.Mutex + currentBlock uint64 +} + +func NewMockBlockCounter() *MockBlockCounter { + return &MockBlockCounter{} +} + +func (mbc *MockBlockCounter) WaitForBlockHeight(blockNumber uint64) error { + panic("unsupported") +} + +func (mbc *MockBlockCounter) BlockHeightWaiter(blockNumber uint64) ( + <-chan uint64, + error, +) { + panic("unsupported") +} + +func (mbc *MockBlockCounter) CurrentBlock() (uint64, error) { + mbc.mutex.Lock() + defer mbc.mutex.Unlock() + + return mbc.currentBlock, nil +} + +func (mbc *MockBlockCounter) SetCurrentBlock(block uint64) { + mbc.mutex.Lock() + defer mbc.mutex.Unlock() + + mbc.currentBlock = block +} + +func (mbc *MockBlockCounter) WatchBlocks(ctx context.Context) <-chan uint64 { panic("unsupported") } diff --git a/pkg/maintainer/wallet/internal/test/marshaling.go b/pkg/maintainer/wallet/internal/test/marshaling.go index dc30fe823d..7c0a887587 100644 --- a/pkg/maintainer/wallet/internal/test/marshaling.go +++ b/pkg/maintainer/wallet/internal/test/marshaling.go @@ -250,6 +250,117 @@ func (psts *ProposeSweepTestScenario) UnmarshalJSON(data []byte) error { return nil } +// UnmarshalJSON implements a custom JSON unmarshaling logic to produce a +// proper FindPendingRedemptionsTestScenario. +func (fprts *FindPendingRedemptionsTestScenario) UnmarshalJSON(data []byte) error { + type findPendingRedemptionsTestScenario struct { + Title string + ChainParameters struct { + AverageBlockTime int64 + CurrentBlock uint64 + RequestTimeout uint32 + RequestMinAge uint32 + } + Filter struct { + WalletPublicKeyHashes []string + WalletsLimit uint16 + RequestsLimit uint16 + RequestAmountLimit uint64 + } + Wallets []struct { + WalletPublicKeyHash string + RegistrationBlockNumber uint64 + } + PendingRedemptions []struct { + WalletPublicKeyHash string + RedeemerOutputScript string + RequestedAmount uint64 + Age int64 + } + ExpectedWalletsPendingRedemptions map[string][]string + } + + var unmarshaled findPendingRedemptionsTestScenario + + err := json.Unmarshal(data, &unmarshaled) + if err != nil { + return err + } + + fprts.Title = unmarshaled.Title + + fprts.ChainParameters.AverageBlockTime = + time.Duration(unmarshaled.ChainParameters.AverageBlockTime) * time.Second + fprts.ChainParameters.CurrentBlock = unmarshaled.ChainParameters.CurrentBlock + fprts.ChainParameters.RequestTimeout = unmarshaled.ChainParameters.RequestTimeout + fprts.ChainParameters.RequestMinAge = unmarshaled.ChainParameters.RequestMinAge + + for _, wpkhString := range unmarshaled.Filter.WalletPublicKeyHashes { + var wpkh [20]byte + copy(wpkh[:], hexToSlice(wpkhString)) + + fprts.Filter.WalletPublicKeyHashes = append( + fprts.Filter.WalletPublicKeyHashes, + wpkh, + ) + } + + fprts.Filter.WalletsLimit = unmarshaled.Filter.WalletsLimit + fprts.Filter.RequestsLimit = unmarshaled.Filter.RequestsLimit + fprts.Filter.RequestAmountLimit = unmarshaled.Filter.RequestAmountLimit + + for _, w := range unmarshaled.Wallets { + var wpkh [20]byte + copy(wpkh[:], hexToSlice(w.WalletPublicKeyHash)) + + fprts.Wallets = append(fprts.Wallets, &Wallet{ + WalletPublicKeyHash: wpkh, + RegistrationBlockNumber: w.RegistrationBlockNumber, + }) + } + + now := time.Now() + currentBlock := fprts.ChainParameters.CurrentBlock + averageBlockTime := fprts.ChainParameters.AverageBlockTime + + for _, pr := range unmarshaled.PendingRedemptions { + var wpkh [20]byte + copy(wpkh[:], hexToSlice(pr.WalletPublicKeyHash)) + + age := time.Duration(pr.Age) * time.Second + ageBlocks := uint64(age.Milliseconds() / averageBlockTime.Milliseconds()) + + requestedAt := now.Add(-age) + requestBlock := currentBlock - ageBlocks + + fprts.PendingRedemptions = append( + fprts.PendingRedemptions, + &RedemptionRequest{ + WalletPublicKeyHash: wpkh, + RedeemerOutputScript: hexToSlice(pr.RedeemerOutputScript), + RequestedAmount: pr.RequestedAmount, + RequestedAt: requestedAt, + RequestBlock: requestBlock, + }, + ) + } + + fprts.ExpectedWalletsPendingRedemptions = make(map[[20]byte][]bitcoin.Script) + for wpkhString, scripts := range unmarshaled.ExpectedWalletsPendingRedemptions { + var wpkh [20]byte + copy(wpkh[:], hexToSlice(wpkhString)) + + convertedScripts := make([]bitcoin.Script, len(scripts)) + for i, script := range scripts { + convertedScripts[i] = hexToSlice(script) + } + + fprts.ExpectedWalletsPendingRedemptions[wpkh] = convertedScripts + } + + return nil +} + func hexToSlice(hexString string) []byte { if len(hexString) == 0 { return []byte{} diff --git a/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_0.json b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_0.json new file mode 100644 index 0000000000..09ea647f9b --- /dev/null +++ b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_0.json @@ -0,0 +1,81 @@ +{ + "Title": "Get all pending redemptions without filtering", + "ChainParameters":{ + "AverageBlockTime": 10, + "CurrentBlock": 100000, + "RequestTimeout": 86400, + "RequestMinAge": 3600 + }, + "Filter": { + "WalletPublicKeyHashes": [], + "WalletsLimit": 0, + "RequestsLimit": 0, + "RequestAmountLimit": 0 + }, + "Wallets": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RegistrationBlockNumber": 5 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RegistrationBlockNumber": 10 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RegistrationBlockNumber": 20 + } + ], + "PendingRedemptions": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000001", + "RequestedAmount": 1000000000, + "Age": 79200 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000002", + "RequestedAmount": 2000000000, + "Age": 75600 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000003", + "RequestedAmount": 3000000000, + "Age": 36000 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000004", + "RequestedAmount": 4000000000, + "Age": 32400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000005", + "RequestedAmount": 5000000000, + "Age": 14400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000006", + "RequestedAmount": 6000000000, + "Age": 10800 + } + ], + "ExpectedWalletsPendingRedemptions": { + "0x928d992e5f5b71de51a1b40fcc4056b99a88a647": [ + "0x00140000000000000000000000000000000000000001", + "0x00140000000000000000000000000000000000000002" + ], + "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e": [ + "0x00140000000000000000000000000000000000000003", + "0x00140000000000000000000000000000000000000004" + ], + "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed": [ + "0x00140000000000000000000000000000000000000005", + "0x00140000000000000000000000000000000000000006" + ] + } +} diff --git a/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_1.json b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_1.json new file mode 100644 index 0000000000..d35ae0fe09 --- /dev/null +++ b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_1.json @@ -0,0 +1,80 @@ +{ + "Title": "Get pending redemptions for specific wallets", + "ChainParameters":{ + "AverageBlockTime": 10, + "CurrentBlock": 100000, + "RequestTimeout": 86400, + "RequestMinAge": 3600 + }, + "Filter": { + "WalletPublicKeyHashes": [ + "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed" + ], + "WalletsLimit": 0, + "RequestsLimit": 0, + "RequestAmountLimit": 0 + }, + "Wallets": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RegistrationBlockNumber": 5 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RegistrationBlockNumber": 10 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RegistrationBlockNumber": 20 + } + ], + "PendingRedemptions": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000001", + "RequestedAmount": 1000000000, + "Age": 79200 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000002", + "RequestedAmount": 2000000000, + "Age": 75600 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000003", + "RequestedAmount": 3000000000, + "Age": 36000 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000004", + "RequestedAmount": 4000000000, + "Age": 32400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000005", + "RequestedAmount": 5000000000, + "Age": 14400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000006", + "RequestedAmount": 6000000000, + "Age": 10800 + } + ], + "ExpectedWalletsPendingRedemptions": { + "0x928d992e5f5b71de51a1b40fcc4056b99a88a647": [ + "0x00140000000000000000000000000000000000000001", + "0x00140000000000000000000000000000000000000002" + ], + "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed": [ + "0x00140000000000000000000000000000000000000005", + "0x00140000000000000000000000000000000000000006" + ] + } +} diff --git a/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_2.json b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_2.json new file mode 100644 index 0000000000..588efe6bb2 --- /dev/null +++ b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_2.json @@ -0,0 +1,73 @@ +{ + "Title": "Get pending redemptions with wallets count limit", + "ChainParameters":{ + "AverageBlockTime": 10, + "CurrentBlock": 100000, + "RequestTimeout": 86400, + "RequestMinAge": 3600 + }, + "Filter": { + "WalletPublicKeyHashes": [], + "WalletsLimit": 1, + "RequestsLimit": 0, + "RequestAmountLimit": 0 + }, + "Wallets": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RegistrationBlockNumber": 5 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RegistrationBlockNumber": 10 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RegistrationBlockNumber": 20 + } + ], + "PendingRedemptions": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000001", + "RequestedAmount": 1000000000, + "Age": 79200 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000002", + "RequestedAmount": 2000000000, + "Age": 75600 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000003", + "RequestedAmount": 3000000000, + "Age": 36000 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000004", + "RequestedAmount": 4000000000, + "Age": 32400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000005", + "RequestedAmount": 5000000000, + "Age": 14400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000006", + "RequestedAmount": 6000000000, + "Age": 10800 + } + ], + "ExpectedWalletsPendingRedemptions": { + "0x928d992e5f5b71de51a1b40fcc4056b99a88a647": [ + "0x00140000000000000000000000000000000000000001", + "0x00140000000000000000000000000000000000000002" + ] + } +} diff --git a/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_3.json b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_3.json new file mode 100644 index 0000000000..eb28d6b861 --- /dev/null +++ b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_3.json @@ -0,0 +1,78 @@ +{ + "Title": "Get pending redemptions with requests count limit", + "ChainParameters":{ + "AverageBlockTime": 10, + "CurrentBlock": 100000, + "RequestTimeout": 86400, + "RequestMinAge": 3600 + }, + "Filter": { + "WalletPublicKeyHashes": [], + "WalletsLimit": 0, + "RequestsLimit": 1, + "RequestAmountLimit": 0 + }, + "Wallets": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RegistrationBlockNumber": 5 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RegistrationBlockNumber": 10 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RegistrationBlockNumber": 20 + } + ], + "PendingRedemptions": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000001", + "RequestedAmount": 1000000000, + "Age": 79200 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000002", + "RequestedAmount": 2000000000, + "Age": 75600 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000003", + "RequestedAmount": 3000000000, + "Age": 36000 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000004", + "RequestedAmount": 4000000000, + "Age": 32400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000005", + "RequestedAmount": 5000000000, + "Age": 14400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000006", + "RequestedAmount": 6000000000, + "Age": 10800 + } + ], + "ExpectedWalletsPendingRedemptions": { + "0x928d992e5f5b71de51a1b40fcc4056b99a88a647": [ + "0x00140000000000000000000000000000000000000001" + ], + "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e": [ + "0x00140000000000000000000000000000000000000003" + ], + "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed": [ + "0x00140000000000000000000000000000000000000005" + ] + } +} diff --git a/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_4.json b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_4.json new file mode 100644 index 0000000000..31fa86e47b --- /dev/null +++ b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_4.json @@ -0,0 +1,77 @@ +{ + "Title": "Get pending redemptions with requests amount limit", + "ChainParameters":{ + "AverageBlockTime": 10, + "CurrentBlock": 100000, + "RequestTimeout": 86400, + "RequestMinAge": 3600 + }, + "Filter": { + "WalletPublicKeyHashes": [], + "WalletsLimit": 0, + "RequestsLimit": 0, + "RequestAmountLimit": 4000000000 + }, + "Wallets": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RegistrationBlockNumber": 5 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RegistrationBlockNumber": 10 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RegistrationBlockNumber": 20 + } + ], + "PendingRedemptions": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000001", + "RequestedAmount": 1000000000, + "Age": 79200 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000002", + "RequestedAmount": 2000000000, + "Age": 75600 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000003", + "RequestedAmount": 3000000000, + "Age": 36000 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000004", + "RequestedAmount": 4000000000, + "Age": 32400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000005", + "RequestedAmount": 5000000000, + "Age": 14400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000006", + "RequestedAmount": 6000000000, + "Age": 10800 + } + ], + "ExpectedWalletsPendingRedemptions": { + "0x928d992e5f5b71de51a1b40fcc4056b99a88a647": [ + "0x00140000000000000000000000000000000000000001", + "0x00140000000000000000000000000000000000000002" + ], + "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e": [ + "0x00140000000000000000000000000000000000000003", + "0x00140000000000000000000000000000000000000004" + ] + } +} diff --git a/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_5.json b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_5.json new file mode 100644 index 0000000000..61b2a19d5c --- /dev/null +++ b/pkg/maintainer/wallet/internal/test/testdata/find_pending_redemptions_scenario_5.json @@ -0,0 +1,79 @@ +{ + "Title": "Get pending redemptions where there are too old and too young ones", + "ChainParameters":{ + "AverageBlockTime": 10, + "CurrentBlock": 100000, + "RequestTimeout": 86400, + "RequestMinAge": 3600 + }, + "Filter": { + "WalletPublicKeyHashes": [], + "WalletsLimit": 0, + "RequestsLimit": 0, + "RequestAmountLimit": 0 + }, + "Wallets": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RegistrationBlockNumber": 5 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RegistrationBlockNumber": 10 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RegistrationBlockNumber": 20 + } + ], + "PendingRedemptions": [ + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000001", + "RequestedAmount": 1000000000, + "Age": 90000 + }, + { + "WalletPublicKeyHash": "0x928d992e5f5b71de51a1b40fcc4056b99a88a647", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000002", + "RequestedAmount": 2000000000, + "Age": 75600 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000003", + "RequestedAmount": 3000000000, + "Age": 36000 + }, + { + "WalletPublicKeyHash": "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000004", + "RequestedAmount": 4000000000, + "Age": 32400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000005", + "RequestedAmount": 5000000000, + "Age": 14400 + }, + { + "WalletPublicKeyHash": "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed", + "RedeemerOutputScript": "0x00140000000000000000000000000000000000000006", + "RequestedAmount": 6000000000, + "Age": 600 + } + ], + "ExpectedWalletsPendingRedemptions": { + "0x928d992e5f5b71de51a1b40fcc4056b99a88a647": [ + "0x00140000000000000000000000000000000000000002" + ], + "0x03b74d6893ad46dfdd01b9e0e3b3385f4fce2d1e": [ + "0x00140000000000000000000000000000000000000003", + "0x00140000000000000000000000000000000000000004" + ], + "0x7670343fc00ccc2d0cd65360e6ad400697ea0fed": [ + "0x00140000000000000000000000000000000000000005" + ] + } +} diff --git a/pkg/maintainer/wallet/internal/test/wallettest.go b/pkg/maintainer/wallet/internal/test/wallettest.go index 6416f2d996..dbecee8295 100644 --- a/pkg/maintainer/wallet/internal/test/wallettest.go +++ b/pkg/maintainer/wallet/internal/test/wallettest.go @@ -16,9 +16,10 @@ import ( ) const ( - testDataDirFormat = "%s/testdata" - findDepositsToSweepTestDataFilePrefix = "find_deposits" - proposeDepositsSweepTestDataFilePrefix = "propose_sweep" + testDataDirFormat = "%s/testdata" + findDepositsToSweepTestDataFilePrefix = "find_deposits" + proposeDepositsSweepTestDataFilePrefix = "propose_sweep" + findPendingRedemptionsTestDataFilePrefix = "find_pending_redemptions" ) // Wallet holds the wallet data in the given test scenario. @@ -58,42 +59,7 @@ type FindDepositsToSweepTestScenario struct { // LoadFindDepositsToSweepTestScenario loads all scenarios related with deposit sweep. func LoadFindDepositsToSweepTestScenario() ([]*FindDepositsToSweepTestScenario, error) { - filePaths, err := detectTestDataFiles(findDepositsToSweepTestDataFilePrefix) - if err != nil { - return nil, fmt.Errorf( - "cannot detect test data files: [%v]", - err, - ) - } - - scenarios := make([]*FindDepositsToSweepTestScenario, 0) - - for _, filePath := range filePaths { - // #nosec G304 (file path provided as taint input) - // This line is used to read a test fixture file. - // There is no user input. - fileBytes, err := ioutil.ReadFile(filePath) - if err != nil { - return nil, fmt.Errorf( - "cannot read file [%v]: [%v]", - filePath, - err, - ) - } - - var scenario FindDepositsToSweepTestScenario - if err = json.Unmarshal(fileBytes, &scenario); err != nil { - return nil, fmt.Errorf( - "cannot unmarshal scenario for file [%v]: [%v]", - filePath, - err, - ) - } - - scenarios = append(scenarios, &scenario) - } - - return scenarios, nil + return loadTestScenarios[*FindDepositsToSweepTestScenario](findDepositsToSweepTestDataFilePrefix) } type ProposeSweepDepositsData struct { @@ -131,7 +97,47 @@ func (psts *ProposeSweepTestScenario) DepositsReferences() []*walletmtr.DepositR // LoadProposeSweepTestScenario loads all scenarios related with deposit sweep. func LoadProposeSweepTestScenario() ([]*ProposeSweepTestScenario, error) { - filePaths, err := detectTestDataFiles(proposeDepositsSweepTestDataFilePrefix) + return loadTestScenarios[*ProposeSweepTestScenario](proposeDepositsSweepTestDataFilePrefix) +} + +// RedemptionRequest holds the redemption request data in the given test scenario. +type RedemptionRequest struct { + WalletPublicKeyHash [20]byte + RedeemerOutputScript bitcoin.Script + RequestedAmount uint64 + RequestedAt time.Time + RequestBlock uint64 +} + +// FindPendingRedemptionsTestScenario represents a test scenario of finding +// pending redemptions. +type FindPendingRedemptionsTestScenario struct { + Title string + ChainParameters struct { + AverageBlockTime time.Duration + CurrentBlock uint64 + RequestTimeout uint32 + RequestMinAge uint32 + } + Filter walletmtr.PendingRedemptionsFilter + Wallets []*Wallet + PendingRedemptions []*RedemptionRequest + ExpectedWalletsPendingRedemptions map[[20]byte][]bitcoin.Script +} + +// LoadFindPendingRedemptionsTestScenario loads all scenarios related with +// finding pending redemptions. +func LoadFindPendingRedemptionsTestScenario() ( + []*FindPendingRedemptionsTestScenario, + error, +) { + return loadTestScenarios[*FindPendingRedemptionsTestScenario]( + findPendingRedemptionsTestDataFilePrefix, + ) +} + +func loadTestScenarios[T json.Unmarshaler](testDataFilePrefix string) ([]T, error) { + filePaths, err := detectTestDataFiles(testDataFilePrefix) if err != nil { return nil, fmt.Errorf( "cannot detect test data files: [%v]", @@ -139,7 +145,7 @@ func LoadProposeSweepTestScenario() ([]*ProposeSweepTestScenario, error) { ) } - scenarios := make([]*ProposeSweepTestScenario, 0) + scenarios := make([]T, 0) for _, filePath := range filePaths { // #nosec G304 (file path provided as taint input) @@ -154,7 +160,7 @@ func LoadProposeSweepTestScenario() ([]*ProposeSweepTestScenario, error) { ) } - var scenario ProposeSweepTestScenario + var scenario T if err = json.Unmarshal(fileBytes, &scenario); err != nil { return nil, fmt.Errorf( "cannot unmarshal scenario for file [%v]: [%v]", @@ -163,7 +169,7 @@ func LoadProposeSweepTestScenario() ([]*ProposeSweepTestScenario, error) { ) } - scenarios = append(scenarios, &scenario) + scenarios = append(scenarios, scenario) } return scenarios, nil diff --git a/pkg/maintainer/wallet/redemptions_test.go b/pkg/maintainer/wallet/redemptions_test.go index f305515e92..e29341b574 100644 --- a/pkg/maintainer/wallet/redemptions_test.go +++ b/pkg/maintainer/wallet/redemptions_test.go @@ -2,9 +2,13 @@ package wallet_test import ( "encoding/hex" + "github.com/go-test/deep" "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" walletmtr "github.com/keep-network/keep-core/pkg/maintainer/wallet" + "github.com/keep-network/keep-core/pkg/maintainer/wallet/internal/test" + "github.com/keep-network/keep-core/pkg/tbtc" + "math/big" "testing" ) @@ -37,3 +41,185 @@ func TestEstimateRedemptionFee(t *testing.T) { expectedFee := 4000 // transactionVirtualSize * satPerVByteFee = 250 * 16 = 4000 testutils.AssertIntsEqual(t, "fee", expectedFee, int(actualFee)) } + +func TestFindPendingRedemptions(t *testing.T) { + scenarios, err := test.LoadFindPendingRedemptionsTestScenario() + if err != nil { + t.Fatal(err) + } + + for _, scenario := range scenarios { + t.Run(scenario.Title, func(t *testing.T) { + tbtcChain := walletmtr.NewLocalChain() + + // Set the average block time enforced by the scenario. + tbtcChain.SetAverageBlockTime(scenario.ChainParameters.AverageBlockTime) + + // Set the scenario's current block using a mock block counter. + // This is needed to build a proper filter for the + // `PastRedemptionRequestedEvents` call. + blockCounter := walletmtr.NewMockBlockCounter() + blockCounter.SetCurrentBlock(scenario.ChainParameters.CurrentBlock) + tbtcChain.SetBlockCounter(blockCounter) + + // Set relevant governable parameters based on values provided by + // the scenario. + tbtcChain.SetRedemptionParameters( + 0, + 0, + 0, + 0, + scenario.ChainParameters.RequestTimeout, + nil, + 0, + ) + tbtcChain.SetRedemptionRequestMinAge(scenario.ChainParameters.RequestMinAge) + + requestTimeoutBlocks := uint64(scenario.ChainParameters.RequestTimeout) / + uint64(scenario.ChainParameters.AverageBlockTime.Seconds()) + + // Record scenario wallets to the local chain. + for _, wallet := range scenario.Wallets { + err := tbtcChain.AddPastNewWalletRegisteredEvent( + nil, + &tbtc.NewWalletRegisteredEvent{ + WalletPublicKeyHash: wallet.WalletPublicKeyHash, + BlockNumber: wallet.RegistrationBlockNumber, + }, + ) + if err != nil { + t.Fatal(err) + } + } + + // Record scenario pending redemptions to the local chain. + for _, pendingRedemption := range scenario.PendingRedemptions { + // Record the corresponding event. Set only relevant fields. + err = tbtcChain.AddPastRedemptionRequestedEvent( + &tbtc.RedemptionRequestedEventFilter{ + // Remember about including the constant factor + // of 1000 blocks. + StartBlock: scenario.ChainParameters.CurrentBlock - requestTimeoutBlocks - 1000, + WalletPublicKeyHash: [][20]byte{pendingRedemption.WalletPublicKeyHash}, + }, + &tbtc.RedemptionRequestedEvent{ + WalletPublicKeyHash: pendingRedemption.WalletPublicKeyHash, + RedeemerOutputScript: pendingRedemption.RedeemerOutputScript, + RequestedAmount: pendingRedemption.RequestedAmount, + BlockNumber: pendingRedemption.RequestBlock, + }, + ) + + // Record the corresponding request object. Set only relevant fields. + tbtcChain.SetPendingRedemptionRequest( + pendingRedemption.WalletPublicKeyHash, + &tbtc.RedemptionRequest{ + RedeemerOutputScript: pendingRedemption.RedeemerOutputScript, + RequestedAmount: pendingRedemption.RequestedAmount, + RequestedAt: pendingRedemption.RequestedAt, + }, + ) + } + + walletsPendingRedemptions, err := walletmtr.FindPendingRedemptions( + tbtcChain, + scenario.Filter, + ) + if err != nil { + t.Fatal(err) + } + + if diff := deep.Equal( + scenario.ExpectedWalletsPendingRedemptions, + walletsPendingRedemptions, + ); diff != nil { + t.Errorf("invalid wallets pending redemptions: %v", diff) + } + }) + } +} + +func TestProposeRedemption(t *testing.T) { + fromHex := func(hexString string) []byte { + bytes, err := hex.DecodeString(hexString) + if err != nil { + t.Fatal(err) + } + return bytes + } + + var walletPublicKeyHash [20]byte + copy(walletPublicKeyHash[:], fromHex("")) + + redeemersOutputScripts := []bitcoin.Script{ + fromHex("00140000000000000000000000000000000000000001"), + fromHex("00140000000000000000000000000000000000000002"), + } + + var tests = map[string]struct { + fee int64 + expectedProposal *tbtc.RedemptionProposal + }{ + "fee provided": { + fee: 10000, + expectedProposal: &tbtc.RedemptionProposal{ + WalletPublicKeyHash: walletPublicKeyHash, + RedeemersOutputScripts: redeemersOutputScripts, + RedemptionTxFee: big.NewInt(10000), + }, + }, + "fee estimated": { + fee: 0, // trigger fee estimation + expectedProposal: &tbtc.RedemptionProposal{ + WalletPublicKeyHash: walletPublicKeyHash, + RedeemersOutputScripts: redeemersOutputScripts, + RedemptionTxFee: big.NewInt(4300), + }, + }, + } + + for testName, test := range tests { + t.Run(testName, func(t *testing.T) { + tbtcChain := walletmtr.NewLocalChain() + btcChain := walletmtr.NewLocalBitcoinChain() + + btcChain.SetEstimateSatPerVByteFee(1, 25) + + for _, script := range redeemersOutputScripts { + tbtcChain.SetPendingRedemptionRequest( + walletPublicKeyHash, + &tbtc.RedemptionRequest{ + RedeemerOutputScript: script, + }, + ) + } + + err := tbtcChain.SetRedemptionProposalValidationResult( + test.expectedProposal, + true, + ) + if err != nil { + t.Fatal(err) + } + + err = walletmtr.ProposeRedemption( + tbtcChain, + btcChain, + walletPublicKeyHash, + test.fee, + redeemersOutputScripts, + false, + ) + if err != nil { + t.Fatal(err) + } + + if diff := deep.Equal( + tbtcChain.RedemptionProposals(), + []*tbtc.RedemptionProposal{test.expectedProposal}, + ); diff != nil { + t.Errorf("invalid deposits: %v", diff) + } + }) + } +}