diff --git a/cmd/coordinator.go b/cmd/coordinator.go index 1ea6d0e0b1..4df4b6dcb4 100644 --- a/cmd/coordinator.go +++ b/cmd/coordinator.go @@ -2,6 +2,11 @@ package cmd import ( "fmt" + "os" + "regexp" + "sort" + "strconv" + "text/tabwriter" "github.com/spf13/cobra" @@ -10,8 +15,8 @@ import ( "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-core/pkg/bitcoin/electrum" "github.com/keep-network/keep-core/pkg/chain/ethereum" - "github.com/keep-network/keep-core/pkg/coordinator" "github.com/keep-network/keep-core/pkg/maintainer/spv" + walletmtr "github.com/keep-network/keep-core/pkg/maintainer/wallet" ) var ( @@ -82,7 +87,10 @@ var listDepositsCommand = cobra.Command{ return fmt.Errorf("failed to find head flag: %v", err) } - _, tbtcChain, _, _, _, err := ethereum.Connect(ctx, clientConfig.Ethereum) + _, tbtcChain, _, _, _, err := ethereum.Connect( + ctx, + clientConfig.Ethereum, + ) if err != nil { return fmt.Errorf( "could not connect to Ethereum chain: [%v]", @@ -100,20 +108,68 @@ var listDepositsCommand = cobra.Command{ var err error walletPublicKeyHash, err = newWalletPublicKeyHash(wallet) if err != nil { - return fmt.Errorf("failed to extract wallet public key hash: %v", err) + return fmt.Errorf( + "failed to extract wallet public key hash: %v", + err, + ) } } - return coordinator.ListDeposits( + deposits, err := walletmtr.FindDeposits( tbtcChain, btcChain, walletPublicKeyHash, head, hideSwept, + false, ) + if err != nil { + return fmt.Errorf( + "failed to get deposits: [%w]", + err, + ) + } + + if len(deposits) == 0 { + return fmt.Errorf("no deposits found") + } + + if err := printDepositsTable(deposits); err != nil { + return fmt.Errorf("failed to print deposits table: %v", err) + } + + return nil }, } +func printDepositsTable(deposits []*walletmtr.Deposit) error { + w := tabwriter.NewWriter(os.Stdout, 2, 4, 1, ' ', tabwriter.AlignRight) + fmt.Fprintf(w, "index\twallet\tvalue (BTC)\tdeposit key\trevealed deposit data\tconfirmations\tswept\t\n") + + for i, deposit := range deposits { + fmt.Fprintf(w, "%d\t%s\t%.5f\t%s\t%s\t%d\t%t\t\n", + i, + hexutils.Encode(deposit.WalletPublicKeyHash[:]), + deposit.AmountBtc, + deposit.DepositKey, + fmt.Sprintf( + "%s:%d:%d", + deposit.FundingTxHash.Hex(bitcoin.ReversedByteOrder), + deposit.FundingOutputIndex, + deposit.RevealBlock, + ), + deposit.Confirmations, + deposit.IsSwept, + ) + } + + if err := w.Flush(); err != nil { + return fmt.Errorf("failed to flush the writer: %v", err) + } + + return nil +} + var proposeDepositsSweepCommand = cobra.Command{ Use: "propose-deposits-sweep", Short: "propose deposits sweep", @@ -121,7 +177,7 @@ var proposeDepositsSweepCommand = cobra.Command{ TraverseChildren: true, Args: func(cmd *cobra.Command, args []string) error { for i, arg := range args { - if err := coordinator.ValidateDepositString(arg); err != nil { + if err := validateDepositReferenceString(arg); err != nil { return fmt.Errorf( "argument [%d] failed validation: %v", i, @@ -183,14 +239,14 @@ var proposeDepositsSweepCommand = cobra.Command{ } } - var deposits []*coordinator.DepositSweepDetails + var deposits []*walletmtr.DepositReference if len(args) > 0 { - deposits, err = coordinator.ParseDepositsToSweep(args) + deposits, err = parseDepositsReferences(args) if err != nil { return fmt.Errorf("failed extract wallet public key hash: %v", err) } } else { - walletPublicKeyHash, deposits, err = coordinator.FindDepositsToSweep( + walletPublicKeyHash, deposits, err = walletmtr.FindDepositsToSweep( tbtcChain, btcChain, walletPublicKeyHash, @@ -209,7 +265,7 @@ var proposeDepositsSweepCommand = cobra.Command{ ) } - return coordinator.ProposeDepositsSweep( + return walletmtr.ProposeDepositsSweep( tbtcChain, btcChain, walletPublicKeyHash, @@ -220,12 +276,93 @@ var proposeDepositsSweepCommand = cobra.Command{ }, } -var proposeDepositsSweepCommandDescription = `Submits a deposits sweep proposal to -the chain. -Expects --wallet and --fee flags along with deposits to sweep provided -as arguments. +var ( + depositsFormatDescription = "Deposits details should be provided as strings containing: \n" + + " - bitcoin transaction hash (unprefixed bitcoin transaction hash in reverse (RPC) order), \n" + + " - bitcoin transaction output index, \n" + + " - ethereum block number when the deposit was revealed to the chain. \n" + + "The properties should be separated by semicolons, in the following format: \n" + + depositReferenceFormatPattern + "\n" + + "e.g. bd99d1d0a61fd104925d9b7ac997958aa8af570418b3fde091f7bfc561608865:1:8392394" + + depositReferenceFormatPattern = ":" + + ":" + + "" + + depositReferenceFormatRegexp = regexp.MustCompile(`^([[:xdigit:]]+):(\d+):(\d+)$`) + + proposeDepositsSweepCommandDescription = "Submits a deposits sweep proposal " + + "to the chain. Expects --wallet and --fee flags along with deposits to " + + "sweep provided as arguments.\n" + + depositsFormatDescription +) + +// parseDepositsReferences decodes a list of deposits references. +func parseDepositsReferences( + depositsRefsStings []string, +) ([]*walletmtr.DepositReference, error) { + depositsRefs := make([]*walletmtr.DepositReference, len(depositsRefsStings)) + + for i, depositRefString := range depositsRefsStings { + matched := depositReferenceFormatRegexp.FindStringSubmatch(depositRefString) + // Check if number of resolved entries match expected number of groups + // for the given regexp. + if len(matched) != 4 { + return nil, fmt.Errorf( + "failed to parse deposit: [%s]", + depositRefString, + ) + } + + txHash, err := bitcoin.NewHashFromString(matched[1], bitcoin.ReversedByteOrder) + if err != nil { + return nil, fmt.Errorf( + "invalid bitcoin transaction hash [%s]: %v", + matched[1], + err, + ) + } + + outputIndex, err := strconv.ParseInt(matched[2], 10, 32) + if err != nil { + return nil, fmt.Errorf( + "invalid bitcoin transaction output index [%s]: %v", + matched[2], + err, + ) + } + + revealBlock, err := strconv.ParseUint(matched[3], 10, 32) + if err != nil { + return nil, fmt.Errorf( + "invalid reveal block number [%s]: %v", + matched[3], + err, + ) + } + + depositsRefs[i] = &walletmtr.DepositReference{ + FundingTxHash: txHash, + FundingOutputIndex: uint32(outputIndex), + RevealBlock: revealBlock, + } + } + + return depositsRefs, nil +} -` + coordinator.DepositsFormatDescription +// validateDepositReferenceString validates format of the string containing a +// deposit reference. +func validateDepositReferenceString(depositRefString string) error { + if !depositReferenceFormatRegexp.MatchString(depositRefString) { + return fmt.Errorf( + "[%s] doesn't match pattern: %s", + depositRefString, + depositReferenceFormatPattern, + ) + } + return nil +} var proposeRedemptionCommand = cobra.Command{ Use: "propose-redemption", @@ -282,7 +419,7 @@ var proposeRedemptionCommand = cobra.Command{ } } - walletPublicKeyHash, redemptions, err := coordinator.FindPendingRedemptions( + walletPublicKeyHash, redemptions, err := walletmtr.FindPendingRedemptions( tbtcChain, walletPublicKeyHash, redemptionMaxSize, @@ -299,7 +436,7 @@ var proposeRedemptionCommand = cobra.Command{ ) } - return coordinator.ProposeRedemption( + return walletmtr.ProposeRedemption( tbtcChain, btcChain, walletPublicKeyHash, @@ -336,12 +473,80 @@ var estimateDepositsSweepFeeCommand = cobra.Command{ return fmt.Errorf("could not connect to Electrum chain: [%v]", err) } - return coordinator.EstimateDepositsSweepFee( + fees, err := walletmtr.EstimateDepositsSweepFee( tbtcChain, btcChain, depositsCount, ) + if err != nil { + return fmt.Errorf("cannot estimate deposits sweep fee: [%v]", err) + } + + err = printDepositsSweepFeeTable(fees) + if err != nil { + return fmt.Errorf("cannot print fees table: [%v]", err) + } + + return nil + }, +} + +// printDepositsSweepFeeTable prints estimated fees for specific deposits counts +// to the standard output. For example: +// +// --------------------------------------------- +// deposits count total fee (satoshis) sat/vbyte +// 1 201 1 +// 2 292 1 +// 3 384 1 +// --------------------------------------------- +func printDepositsSweepFeeTable( + fees map[int]struct { + TotalFee int64 + SatPerVByteFee int64 }, +) error { + writer := tabwriter.NewWriter( + os.Stdout, + 2, + 4, + 1, + ' ', + tabwriter.AlignRight, + ) + + _, err := fmt.Fprintf(writer, "deposits count\ttotal fee (satoshis)\tsat/vbyte\t\n") + if err != nil { + return err + } + + var depositsCountKeys []int + for depositsCountKey := range fees { + depositsCountKeys = append(depositsCountKeys, depositsCountKey) + } + + sort.Slice(depositsCountKeys, func(i, j int) bool { + return depositsCountKeys[i] < depositsCountKeys[j] + }) + + for _, depositsCountKey := range depositsCountKeys { + _, err := fmt.Fprintf( + writer, + "%v\t%v\t%v\t\n", + depositsCountKey, + fees[depositsCountKey].TotalFee, + fees[depositsCountKey].SatPerVByteFee, + ) + if err != nil { + return err + } + } + + if err := writer.Flush(); err != nil { + return fmt.Errorf("failed to flush the writer: %v", err) + } + + return nil } var estimateDepositsSweepFeeCommandDescription = "Estimates the satoshi " + diff --git a/cmd/flags.go b/cmd/flags.go index 6c0a668534..31cf4d65fa 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -343,9 +343,9 @@ func initMaintainerFlags(command *cobra.Command, cfg *config.Config) { ) command.Flags().DurationVar( - &cfg.Maintainer.WalletCoordination.SweepInterval, - "walletCoordination.sweepInterval", - wallet.DefaultSweepInterval, + &cfg.Maintainer.WalletCoordination.DepositSweepInterval, + "walletCoordination.depositSweepInterval", + wallet.DefaultDepositSweepInterval, "The time interval in which unswept deposits are checked.", ) diff --git a/cmd/flags_test.go b/cmd/flags_test.go index e63132f4dd..bd62ae675b 100644 --- a/cmd/flags_test.go +++ b/cmd/flags_test.go @@ -253,9 +253,9 @@ var cmdFlagsTests = map[string]struct { expectedValueFromFlag: 7 * time.Hour, defaultValue: 3 * time.Hour, }, - "maintainer.walletCoordination.sweepInterval": { - readValueFunc: func(c *config.Config) interface{} { return c.Maintainer.WalletCoordination.SweepInterval }, - flagName: "--walletCoordination.sweepInterval", + "maintainer.walletCoordination.depositSweepInterval": { + readValueFunc: func(c *config.Config) interface{} { return c.Maintainer.WalletCoordination.DepositSweepInterval }, + flagName: "--walletCoordination.depositSweepInterval", flagValue: "35h", expectedValueFromFlag: 35 * time.Hour, defaultValue: 48 * time.Hour, diff --git a/config/config_test.go b/config/config_test.go index ed49b0c433..48cffff955 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -215,8 +215,8 @@ func TestReadConfigFromFile(t *testing.T) { readValueFunc: func(c *Config) interface{} { return c.Maintainer.WalletCoordination.RedemptionInterval }, expectedValue: 13 * time.Hour, }, - "Maintainer.WalletCoordination.SweepInterval": { - readValueFunc: func(c *Config) interface{} { return c.Maintainer.WalletCoordination.SweepInterval }, + "Maintainer.WalletCoordination.DepositSweepInterval": { + readValueFunc: func(c *Config) interface{} { return c.Maintainer.WalletCoordination.DepositSweepInterval }, expectedValue: 64 * time.Hour, }, "Maintainer.Spv.Enabled": { diff --git a/pkg/chain/ethereum/tbtc.go b/pkg/chain/ethereum/tbtc.go index 5f1af85e8b..05ac6fe249 100644 --- a/pkg/chain/ethereum/tbtc.go +++ b/pkg/chain/ethereum/tbtc.go @@ -10,13 +10,11 @@ import ( "sort" "time" - "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/coordinator" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" + "github.com/keep-network/keep-common/pkg/chain/ethereum/ethutil" + "github.com/keep-network/keep-core/pkg/bitcoin" "github.com/keep-network/keep-common/pkg/chain/ethereum" "github.com/keep-network/keep-core/pkg/chain" @@ -1124,8 +1122,8 @@ func (tc *TbtcChain) GetDepositRequest( } func (tc *TbtcChain) PastNewWalletRegisteredEvents( - filter *coordinator.NewWalletRegisteredEventFilter, -) ([]*coordinator.NewWalletRegisteredEvent, error) { + filter *tbtc.NewWalletRegisteredEventFilter, +) ([]*tbtc.NewWalletRegisteredEvent, error) { var startBlock uint64 var endBlock *uint64 var ecdsaWalletID [][32]byte @@ -1148,9 +1146,9 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents( return nil, err } - convertedEvents := make([]*coordinator.NewWalletRegisteredEvent, 0) + convertedEvents := make([]*tbtc.NewWalletRegisteredEvent, 0) for _, event := range events { - convertedEvent := &coordinator.NewWalletRegisteredEvent{ + convertedEvent := &tbtc.NewWalletRegisteredEvent{ EcdsaWalletID: event.EcdsaWalletID, WalletPublicKeyHash: event.WalletPubKeyHash, BlockNumber: event.Raw.BlockNumber, diff --git a/pkg/coordinator/chain.go b/pkg/coordinator/chain.go deleted file mode 100644 index ab7e93eb92..0000000000 --- a/pkg/coordinator/chain.go +++ /dev/null @@ -1,150 +0,0 @@ -package coordinator - -import ( - "math/big" - - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/tbtc" -) - -// NewWalletRegisteredEvent represents a new wallet registered event. -type NewWalletRegisteredEvent struct { - EcdsaWalletID [32]byte - WalletPublicKeyHash [20]byte - BlockNumber uint64 -} - -// NewWalletRegisteredEventFilter is a component allowing to filter NewWalletRegisteredEvent. -type NewWalletRegisteredEventFilter struct { - StartBlock uint64 - EndBlock *uint64 - EcdsaWalletID [][32]byte - WalletPublicKeyHash [][20]byte -} - -// Chain represents the interface that the coordinator module expects to interact -// with the anchoring blockchain on. -type Chain interface { - // GetDepositRequest gets the on-chain deposit request for the given - // funding transaction hash and output index.The returned values represent: - // - deposit request which is non-nil only when the deposit request was - // found, - // - boolean value which is true if the deposit request was found, false - // otherwise, - // - error which is non-nil only when the function execution failed. It will - // be nil if the deposit request was not found, but the function execution - // succeeded. - GetDepositRequest( - fundingTxHash bitcoin.Hash, - fundingOutputIndex uint32, - ) (*tbtc.DepositChainRequest, bool, error) - - // PastNewWalletRegisteredEvents fetches past new wallet registered 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. - PastNewWalletRegisteredEvents( - filter *NewWalletRegisteredEventFilter, - ) ([]*NewWalletRegisteredEvent, error) - - // BuildDepositKey calculates a deposit key for the given funding transaction - // which is a unique identifier for a deposit on-chain. - BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int - - // GetDepositParameters gets the current value of parameters relevant - // for the depositing process. - GetDepositParameters() ( - dustThreshold uint64, - treasuryFeeDivisor uint64, - txMaxFee uint64, - revealAheadPeriod uint32, - err error, - ) - - // SubmitDepositSweepProposalWithReimbursement submits a deposit sweep - // proposal to the chain. It reimburses the gas cost to the caller. - SubmitDepositSweepProposalWithReimbursement( - proposal *tbtc.DepositSweepProposal, - ) error - - // GetDepositSweepMaxSize gets the maximum number of deposits that can - // be part of a deposit sweep proposal. - GetDepositSweepMaxSize() (uint16, error) - - // PastRedemptionRequestedEvents fetches past redemption requested 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. - PastRedemptionRequestedEvents( - filter *tbtc.RedemptionRequestedEventFilter, - ) ([]*tbtc.RedemptionRequestedEvent, error) - - // BuildRedemptionKey calculates a redemption key for the given redemption - // request which is an identifier for a redemption at the given time - // on-chain. - BuildRedemptionKey( - walletPublicKeyHash [20]byte, - redeemerOutputScript bitcoin.Script, - ) (*big.Int, error) - - // GetRedemptionParameters gets the current value of parameters relevant - // for the redemption process. - GetRedemptionParameters() ( - dustThreshold uint64, - treasuryFeeDivisor uint64, - txMaxFee uint64, - txMaxTotalFee uint64, - timeout uint32, - timeoutSlashingAmount *big.Int, - timeoutNotifierRewardMultiplier uint32, - err error, - ) - - // SubmitRedemptionProposalWithReimbursement submits a redemption proposal - // to the chain. It reimburses the gas cost to the caller. - SubmitRedemptionProposalWithReimbursement( - proposal *tbtc.RedemptionProposal, - ) error - - // GetRedemptionMaxSize gets the maximum number of redemption requests that - // can be a part of a redemption sweep proposal. - GetRedemptionMaxSize() (uint16, error) - - // GetRedemptionRequestMinAge get the minimum time that must elapse since - // the redemption request creation before a request becomes eligible for - // a processing. - GetRedemptionRequestMinAge() (uint32, error) - - // PastDepositRevealedEvents fetches past deposit reveal 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. - PastDepositRevealedEvents( - filter *tbtc.DepositRevealedEventFilter, - ) ([]*tbtc.DepositRevealedEvent, error) - - // GetPendingRedemptionRequest gets the on-chain pending redemption request - // for the given wallet public key hash and redeemer output script. - // Returns an error if the request was not found. - GetPendingRedemptionRequest( - walletPublicKeyHash [20]byte, - redeemerOutputScript bitcoin.Script, - ) (*tbtc.RedemptionRequest, error) - - // ValidateDepositSweepProposal validates the given deposit sweep proposal - // against the chain. It requires some additional data about the deposits - // that must be fetched externally. Returns an error if the proposal is - // not valid or nil otherwise. - ValidateDepositSweepProposal( - proposal *tbtc.DepositSweepProposal, - depositsExtraInfo []struct { - *tbtc.Deposit - FundingTx *bitcoin.Transaction - }, - ) error - - // ValidateRedemptionProposal validates the given redemption proposal - // against the chain. Returns an error if the proposal is not valid or - // nil otherwise. - ValidateRedemptionProposal(proposal *tbtc.RedemptionProposal) error -} diff --git a/pkg/coordinator/chain_test.go b/pkg/coordinator/chain_test.go deleted file mode 100644 index 6d429d6774..0000000000 --- a/pkg/coordinator/chain_test.go +++ /dev/null @@ -1,405 +0,0 @@ -package coordinator_test - -import ( - "bytes" - "crypto/sha256" - "encoding/binary" - "encoding/hex" - "fmt" - "math/big" - "sync" - - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/coordinator" - "github.com/keep-network/keep-core/pkg/tbtc" -) - -type localChain struct { - mutex sync.Mutex - - depositRequests map[[32]byte]*tbtc.DepositChainRequest - pastDepositRevealedEvents map[[32]byte][]*tbtc.DepositRevealedEvent - pastNewWalletRegisteredEvents map[[32]byte][]*coordinator.NewWalletRegisteredEvent - depositParameters depositParameters - depositSweepProposalValidations map[[32]byte]bool - depositSweepProposals []*tbtc.DepositSweepProposal -} - -type depositParameters = struct { - dustThreshold uint64 - treasuryFeeDivisor uint64 - txMaxFee uint64 - revealAheadPeriod uint32 -} - -func newLocalTbtcChain() *localChain { - return &localChain{ - depositRequests: make(map[[32]byte]*tbtc.DepositChainRequest), - pastDepositRevealedEvents: make(map[[32]byte][]*tbtc.DepositRevealedEvent), - pastNewWalletRegisteredEvents: make(map[[32]byte][]*coordinator.NewWalletRegisteredEvent), - depositSweepProposalValidations: make(map[[32]byte]bool), - } -} - -func (lc *localChain) PastDepositRevealedEvents( - filter *tbtc.DepositRevealedEventFilter, -) ([]*tbtc.DepositRevealedEvent, error) { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - eventsKey, err := buildPastDepositRevealedEventsKey(filter) - if err != nil { - return nil, err - } - - events, ok := lc.pastDepositRevealedEvents[eventsKey] - if !ok { - return nil, fmt.Errorf("no events for given filter") - } - - return events, nil -} - -func (lc *localChain) addPastDepositRevealedEvent( - filter *tbtc.DepositRevealedEventFilter, - event *tbtc.DepositRevealedEvent, -) error { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - eventsKey, err := buildPastDepositRevealedEventsKey(filter) - if err != nil { - return err - } - - lc.pastDepositRevealedEvents[eventsKey] = append( - lc.pastDepositRevealedEvents[eventsKey], - event, - ) - - return nil -} - -func buildPastDepositRevealedEventsKey( - filter *tbtc.DepositRevealedEventFilter, -) ([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 _, depositor := range filter.Depositor { - depositorBytes, err := hex.DecodeString(depositor.String()) - if err != nil { - return [32]byte{}, err - } - - buffer.Write(depositorBytes) - } - - for _, walletPublicKeyHash := range filter.WalletPublicKeyHash { - buffer.Write(walletPublicKeyHash[:]) - } - - return sha256.Sum256(buffer.Bytes()), nil -} - -func (lc *localChain) GetDepositRequest( - fundingTxHash bitcoin.Hash, - fundingOutputIndex uint32, -) (*tbtc.DepositChainRequest, bool, error) { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - requestKey := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) - - request, ok := lc.depositRequests[requestKey] - if !ok { - return nil, false, nil - } - - return request, true, nil -} - -func (lc *localChain) setDepositRequest( - fundingTxHash bitcoin.Hash, - fundingOutputIndex uint32, - request *tbtc.DepositChainRequest, -) { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - requestKey := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) - - lc.depositRequests[requestKey] = request -} - -func (lc *localChain) PastNewWalletRegisteredEvents( - filter *coordinator.NewWalletRegisteredEventFilter, -) ([]*coordinator.NewWalletRegisteredEvent, error) { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - eventsKey, err := buildPastNewWalletRegisteredEventsKey(filter) - if err != nil { - return nil, err - } - - events, ok := lc.pastNewWalletRegisteredEvents[eventsKey] - if !ok { - return nil, fmt.Errorf("no events for given filter") - } - - return events, nil -} - -func (lc *localChain) addPastNewWalletRegisteredEvent( - filter *coordinator.NewWalletRegisteredEventFilter, - event *coordinator.NewWalletRegisteredEvent, -) error { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - eventsKey, err := buildPastNewWalletRegisteredEventsKey(filter) - if err != nil { - return err - } - - if _, ok := lc.pastNewWalletRegisteredEvents[eventsKey]; !ok { - lc.pastNewWalletRegisteredEvents[eventsKey] = []*coordinator.NewWalletRegisteredEvent{} - } - - lc.pastNewWalletRegisteredEvents[eventsKey] = append( - lc.pastNewWalletRegisteredEvents[eventsKey], - event, - ) - - return nil -} - -func buildPastNewWalletRegisteredEventsKey( - filter *coordinator.NewWalletRegisteredEventFilter, -) ([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 _, ecdsaWalletID := range filter.EcdsaWalletID { - buffer.Write(ecdsaWalletID[:]) - } - - for _, walletPublicKeyHash := range filter.WalletPublicKeyHash { - buffer.Write(walletPublicKeyHash[:]) - } - - return sha256.Sum256(buffer.Bytes()), nil -} - -func (lc *localChain) PastRedemptionRequestedEvents( - filter *tbtc.RedemptionRequestedEventFilter, -) ([]*tbtc.RedemptionRequestedEvent, error) { - panic("unsupported") -} - -func (lc *localChain) BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int { - depositKeyBytes := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) - - return new(big.Int).SetBytes(depositKeyBytes[:]) -} - -func buildDepositRequestKey( - fundingTxHash bitcoin.Hash, - fundingOutputIndex uint32, -) [32]byte { - fundingOutputIndexBytes := make([]byte, 4) - binary.BigEndian.PutUint32(fundingOutputIndexBytes, fundingOutputIndex) - - return sha256.Sum256(append(fundingTxHash[:], fundingOutputIndexBytes...)) -} - -func (lc *localChain) BuildRedemptionKey( - walletPublicKeyHash [20]byte, - redeemerOutputScript bitcoin.Script, -) (*big.Int, error) { - panic("unsupported") -} - -func (lc *localChain) GetDepositParameters() ( - dustThreshold uint64, - treasuryFeeDivisor uint64, - txMaxFee uint64, - revealAheadPeriod uint32, - err error, -) { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - return lc.depositParameters.dustThreshold, - lc.depositParameters.treasuryFeeDivisor, - lc.depositParameters.txMaxFee, - lc.depositParameters.revealAheadPeriod, - nil -} - -func (lc *localChain) GetPendingRedemptionRequest( - walletPublicKeyHash [20]byte, - redeemerOutputScript bitcoin.Script, -) (*tbtc.RedemptionRequest, error) { - panic("unsupported") -} - -func (lc *localChain) setDepositParameters( - dustThreshold uint64, - treasuryFeeDivisor uint64, - txMaxFee uint64, - revealAheadPeriod uint32, -) { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - lc.depositParameters = depositParameters{ - dustThreshold: dustThreshold, - treasuryFeeDivisor: treasuryFeeDivisor, - txMaxFee: txMaxFee, - revealAheadPeriod: revealAheadPeriod, - } -} - -func (lc *localChain) GetRedemptionParameters() ( - dustThreshold uint64, - treasuryFeeDivisor uint64, - txMaxFee uint64, - txMaxTotalFee uint64, - timeout uint32, - timeoutSlashingAmount *big.Int, - timeoutNotifierRewardMultiplier uint32, - err error, -) { - panic("unsupported") -} - -func (lc *localChain) ValidateDepositSweepProposal( - proposal *tbtc.DepositSweepProposal, - depositsExtraInfo []struct { - *tbtc.Deposit - FundingTx *bitcoin.Transaction - }, -) error { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - key, err := buildDepositSweepProposalValidationKey(proposal) - if err != nil { - return err - } - - result, ok := lc.depositSweepProposalValidations[key] - if !ok { - return fmt.Errorf("validation result unknown") - } - - if !result { - return fmt.Errorf("validation failed") - } - - return nil -} - -func (lc *localChain) setDepositSweepProposalValidationResult( - proposal *tbtc.DepositSweepProposal, - depositsExtraInfo []struct { - *tbtc.Deposit - FundingTx *bitcoin.Transaction - }, - result bool, -) error { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - key, err := buildDepositSweepProposalValidationKey(proposal) - if err != nil { - return err - } - - lc.depositSweepProposalValidations[key] = result - - return nil -} - -func buildDepositSweepProposalValidationKey( - proposal *tbtc.DepositSweepProposal, -) ([32]byte, error) { - var buffer bytes.Buffer - - buffer.Write(proposal.WalletPublicKeyHash[:]) - - for _, deposit := range proposal.DepositsKeys { - buffer.Write(deposit.FundingTxHash[:]) - - fundingOutputIndex := make([]byte, 4) - binary.BigEndian.PutUint32(fundingOutputIndex, deposit.FundingOutputIndex) - buffer.Write(fundingOutputIndex) - } - - buffer.Write(proposal.SweepTxFee.Bytes()) - - return sha256.Sum256(buffer.Bytes()), nil -} - -func (lc *localChain) SubmitDepositSweepProposalWithReimbursement( - proposal *tbtc.DepositSweepProposal, -) error { - lc.mutex.Lock() - defer lc.mutex.Unlock() - - lc.depositSweepProposals = append(lc.depositSweepProposals, proposal) - - return nil -} - -func (lc *localChain) SubmitRedemptionProposalWithReimbursement( - proposal *tbtc.RedemptionProposal, -) error { - panic("unsupported") -} - -func (lc *localChain) GetDepositSweepMaxSize() (uint16, error) { - panic("unsupported") -} - -func (lc *localChain) ValidateRedemptionProposal( - proposal *tbtc.RedemptionProposal, -) error { - panic("unsupported") -} - -func (lc *localChain) GetRedemptionMaxSize() (uint16, error) { - panic("unsupported") -} - -func (lc *localChain) GetRedemptionRequestMinAge() (uint32, error) { - panic("unsupported") -} diff --git a/pkg/coordinator/coordinator.go b/pkg/coordinator/coordinator.go deleted file mode 100644 index fee65ecae5..0000000000 --- a/pkg/coordinator/coordinator.go +++ /dev/null @@ -1,7 +0,0 @@ -package coordinator - -import ( - "github.com/ipfs/go-log" -) - -var logger = log.Logger("keep-coordinator") diff --git a/pkg/coordinator/deposits.go b/pkg/coordinator/deposits.go deleted file mode 100644 index ace890fe7e..0000000000 --- a/pkg/coordinator/deposits.go +++ /dev/null @@ -1,330 +0,0 @@ -package coordinator - -import ( - "fmt" - "os" - "sort" - "text/tabwriter" - - "github.com/keep-network/keep-core/internal/hexutils" - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/tbtc" -) - -type depositEntry struct { - walletPublicKeyHash [20]byte - - depositKey string - revealBlock uint64 - isSwept bool - - fundingTxHash bitcoin.Hash - fundingOutputIndex uint32 - amountBtc float64 - confirmations uint -} - -// ListDeposits gets deposits from the chain and prints them to standard output. -func ListDeposits( - chain Chain, - btcChain bitcoin.Chain, - walletPublicKeyHash [20]byte, - head int, - skipSwept bool, -) error { - deposits, err := getDeposits( - chain, - btcChain, - walletPublicKeyHash, - head, - skipSwept, - false, - ) - if err != nil { - return fmt.Errorf( - "failed to get deposits: [%w]", - err, - ) - } - - if len(deposits) == 0 { - return fmt.Errorf("no deposits found") - } - - // Print - if err := printTable(deposits); err != nil { - return fmt.Errorf("failed to print deposits table: %v", err) - } - - return nil -} - -// FindDepositsToSweep finds deposits that can be swept. -// If a wallet public key hash is provided, it will find unswept deposits for the -// given wallet. If a wallet public key hash is nil, it will check all wallets -// starting from the oldest one to find a first wallet containing unswept deposits -// and return those deposits. -// maxNumberOfDeposits is used as a ceiling for the number of deposits in the -// result. If number of discovered deposits meets the maxNumberOfDeposits the -// function will stop fetching more deposits. -// This function will return a wallet public key hash and a list of deposits from -// the wallet that can be swept. -// Deposits with insufficient number of funding transaction confirmations will -// not be taken into consideration for sweeping. -// The result will not mix deposits for different wallets. -// TODO: Cache immutable data -func FindDepositsToSweep( - chain Chain, - btcChain bitcoin.Chain, - walletPublicKeyHash [20]byte, - maxNumberOfDeposits uint16, -) ([20]byte, []*DepositSweepDetails, error) { - logger.Infof("deposit sweep max size: %d", maxNumberOfDeposits) - - getDepositsToSweepFromWallet := func(walletToSweep [20]byte) ([]depositEntry, error) { - unsweptDeposits, err := getDeposits( - chain, - btcChain, - walletToSweep, - int(maxNumberOfDeposits), - true, - true, - ) - if err != nil { - return nil, - fmt.Errorf( - "failed to get deposits for [%s] wallet: [%w]", - walletToSweep, - err, - ) - } - return unsweptDeposits, nil - } - - var depositsToSweep []depositEntry - // If walletPublicKeyHash is not provided we need to find a wallet that has - // unswept deposits. - if walletPublicKeyHash == [20]byte{} { - walletRegisteredEvents, err := chain.PastNewWalletRegisteredEvents(nil) - if err != nil { - return [20]byte{}, nil, fmt.Errorf("failed to get registered wallets: [%w]", err) - } - - // Take the oldest first - sort.SliceStable(walletRegisteredEvents, func(i, j int) bool { - return walletRegisteredEvents[i].BlockNumber < walletRegisteredEvents[j].BlockNumber - }) - - sweepingWallets := walletRegisteredEvents - // Only two the most recently created wallets are sweeping. - if len(walletRegisteredEvents) >= 2 { - sweepingWallets = walletRegisteredEvents[len(walletRegisteredEvents)-2:] - } - - for _, registeredWallet := range sweepingWallets { - logger.Infof( - "fetching deposits from wallet [%s]...", - hexutils.Encode(registeredWallet.WalletPublicKeyHash[:]), - ) - - unsweptDeposits, err := getDepositsToSweepFromWallet( - registeredWallet.WalletPublicKeyHash, - ) - if err != nil { - return [20]byte{}, nil, err - } - - // Check if there are any unswept deposits in this wallet. If so - // sweep this wallet and don't check the other wallet. - if len(unsweptDeposits) > 0 { - walletPublicKeyHash = registeredWallet.WalletPublicKeyHash - depositsToSweep = unsweptDeposits - break - } - } - } else { - logger.Infof( - "fetching deposits from wallet [%s]...", - hexutils.Encode(walletPublicKeyHash[:]), - ) - unsweptDeposits, err := getDepositsToSweepFromWallet( - walletPublicKeyHash, - ) - if err != nil { - return [20]byte{}, nil, err - } - depositsToSweep = unsweptDeposits - } - - if len(depositsToSweep) == 0 { - return [20]byte{}, nil, nil - } - - logger.Infof( - "found [%d] deposits to sweep for wallet [%s]", - len(depositsToSweep), - hexutils.Encode(walletPublicKeyHash[:]), - ) - - for i, deposit := range depositsToSweep { - logger.Infof( - "deposit [%d/%d] - %s", - i+1, - len(depositsToSweep), - fmt.Sprintf( - "depositKey: [%s], reveal block: [%d], funding transaction: [%s], output index: [%d]", - deposit.depositKey, - deposit.revealBlock, - deposit.fundingTxHash.Hex(bitcoin.ReversedByteOrder), - deposit.fundingOutputIndex, - )) - } - - result := make([]*DepositSweepDetails, len(depositsToSweep)) - for i, deposit := range depositsToSweep { - result[i] = &DepositSweepDetails{ - FundingTxHash: deposit.fundingTxHash, - FundingOutputIndex: deposit.fundingOutputIndex, - RevealBlock: deposit.revealBlock, - } - } - - return walletPublicKeyHash, result, nil -} - -func getDeposits( - chain Chain, - btcChain bitcoin.Chain, - walletPublicKeyHash [20]byte, - maxNumberOfDeposits int, - skipSwept bool, - skipUnconfirmed bool, -) ([]depositEntry, error) { - logger.Infof("reading revealed deposits from chain...") - - filter := &tbtc.DepositRevealedEventFilter{} - if walletPublicKeyHash != [20]byte{} { - filter.WalletPublicKeyHash = [][20]byte{walletPublicKeyHash} - } - - depositRevealedEvents, err := chain.PastDepositRevealedEvents(filter) - if err != nil { - return []depositEntry{}, fmt.Errorf( - "failed to get past deposit revealed events: [%w]", - err, - ) - } - - logger.Infof("found %d DepositRevealed events", len(depositRevealedEvents)) - - // Take the oldest first - sort.SliceStable(depositRevealedEvents, func(i, j int) bool { - return depositRevealedEvents[i].BlockNumber < depositRevealedEvents[j].BlockNumber - }) - - logger.Infof("getting deposits details...") - - resultSliceCapacity := len(depositRevealedEvents) - if maxNumberOfDeposits > 0 { - resultSliceCapacity = maxNumberOfDeposits - } - - result := make([]depositEntry, 0, resultSliceCapacity) - for i, event := range depositRevealedEvents { - if len(result) == cap(result) { - break - } - - logger.Debugf("getting details of deposit %d/%d", i+1, len(depositRevealedEvents)) - - depositKey := chain.BuildDepositKey(event.FundingTxHash, event.FundingOutputIndex) - - depositRequest, found, err := chain.GetDepositRequest( - event.FundingTxHash, - event.FundingOutputIndex, - ) - if err != nil { - return result, fmt.Errorf( - "failed to get deposit request: [%w]", - err, - ) - } - - if !found { - return nil, fmt.Errorf( - "no deposit request for key [0x%x]", - depositKey.Text(16), - ) - } - - isSwept := depositRequest.SweptAt.Unix() != 0 - if skipSwept && isSwept { - logger.Debugf("deposit %d/%d is already swept", i+1, len(depositRevealedEvents)) - continue - } - - confirmations, err := btcChain.GetTransactionConfirmations(event.FundingTxHash) - if err != nil { - logger.Errorf( - "failed to get bitcoin transaction confirmations: [%v]", - err, - ) - } - - if skipUnconfirmed && confirmations < tbtc.DepositSweepRequiredFundingTxConfirmations { - logger.Debugf( - "deposit %d/%d funding transaction doesn't have enough confirmations: %d/%d", - i+1, len(depositRevealedEvents), - confirmations, tbtc.DepositSweepRequiredFundingTxConfirmations) - continue - } - - result = append( - result, - depositEntry{ - walletPublicKeyHash: event.WalletPublicKeyHash, - depositKey: hexutils.Encode(depositKey.Bytes()), - revealBlock: event.BlockNumber, - isSwept: isSwept, - fundingTxHash: event.FundingTxHash, - fundingOutputIndex: event.FundingOutputIndex, - amountBtc: convertSatToBtc(float64(depositRequest.Amount)), - confirmations: confirmations, - }, - ) - } - - return result, nil -} - -func printTable(deposits []depositEntry) error { - w := tabwriter.NewWriter(os.Stdout, 2, 4, 1, ' ', tabwriter.AlignRight) - fmt.Fprintf(w, "index\twallet\tvalue (BTC)\tdeposit key\trevealed deposit data\tconfirmations\tswept\t\n") - - for i, deposit := range deposits { - fmt.Fprintf(w, "%d\t%s\t%.5f\t%s\t%s\t%d\t%t\t\n", - i, - hexutils.Encode(deposit.walletPublicKeyHash[:]), - deposit.amountBtc, - deposit.depositKey, - fmt.Sprintf( - "%s:%d:%d", - deposit.fundingTxHash.Hex(bitcoin.ReversedByteOrder), - deposit.fundingOutputIndex, - deposit.revealBlock, - ), - deposit.confirmations, - deposit.isSwept, - ) - } - - if err := w.Flush(); err != nil { - return fmt.Errorf("failed to flush the writer: %v", err) - } - - return nil -} - -func convertSatToBtc(sats float64) float64 { - return sats / float64(100000000) -} diff --git a/pkg/coordinator/deposits_test.go b/pkg/coordinator/deposits_test.go deleted file mode 100644 index 066a5b3232..0000000000 --- a/pkg/coordinator/deposits_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package coordinator_test - -import ( - "testing" - - "github.com/go-test/deep" - "github.com/ipfs/go-log" - - "github.com/keep-network/keep-core/pkg/coordinator" - "github.com/keep-network/keep-core/pkg/coordinator/internal/test" - - "github.com/keep-network/keep-core/internal/hexutils" - "github.com/keep-network/keep-core/pkg/tbtc" -) - -func TestFindDepositsToSweep(t *testing.T) { - log.SetLogLevel("*", "DEBUG") - - scenarios, err := test.LoadFindDepositsToSweepTestScenario() - if err != nil { - t.Fatal(err) - } - - for _, scenario := range scenarios { - t.Run(scenario.Title, func(t *testing.T) { - tbtcChain := newLocalTbtcChain() - btcChain := newLocalBitcoinChain() - - expectedWallet := scenario.ExpectedWalletPublicKeyHash - - // Chain setup. - for _, wallet := range scenario.Wallets { - tbtcChain.addPastNewWalletRegisteredEvent( - nil, - &coordinator.NewWalletRegisteredEvent{ - WalletPublicKeyHash: wallet.WalletPublicKeyHash, - BlockNumber: wallet.RegistrationBlockNumber, - }, - ) - - } - - for _, deposit := range scenario.Deposits { - tbtcChain.setDepositRequest( - deposit.FundingTxHash, - deposit.FundingOutputIndex, - &tbtc.DepositChainRequest{SweptAt: deposit.SweptAt}, - ) - btcChain.setTransactionConfirmations( - deposit.FundingTxHash, - deposit.FundingTxConfirmations, - ) - - tbtcChain.addPastDepositRevealedEvent( - &tbtc.DepositRevealedEventFilter{WalletPublicKeyHash: [][20]byte{deposit.WalletPublicKeyHash}}, - &tbtc.DepositRevealedEvent{ - BlockNumber: deposit.RevealBlockNumber, - WalletPublicKeyHash: deposit.WalletPublicKeyHash, - FundingTxHash: deposit.FundingTxHash, - FundingOutputIndex: deposit.FundingOutputIndex, - }, - ) - } - - // Test execution. - actualWallet, actualDeposits, err := coordinator.FindDepositsToSweep( - tbtcChain, - btcChain, - scenario.WalletPublicKeyHash, - scenario.MaxNumberOfDeposits, - ) - - if err != nil { - t.Fatal(err) - } - - if actualWallet != expectedWallet { - t.Errorf( - "invalid wallet public key hash\nexpected: %s\nactual: %s", - hexutils.Encode(expectedWallet[:]), - hexutils.Encode(actualWallet[:]), - ) - } - - if diff := deep.Equal(actualDeposits, scenario.ExpectedUnsweptDeposits); diff != nil { - t.Errorf("invalid deposits: %v", diff) - } - }) - } -} diff --git a/pkg/coordinator/sweep.go b/pkg/coordinator/sweep.go deleted file mode 100644 index 9bbee4d7dc..0000000000 --- a/pkg/coordinator/sweep.go +++ /dev/null @@ -1,339 +0,0 @@ -package coordinator - -import ( - "fmt" - "math" - "math/big" - "os" - "regexp" - "sort" - "strconv" - "text/tabwriter" - - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/tbtc" -) - -type btcTransaction = struct { - FundingTxHash bitcoin.Hash - FundingOutputIndex uint32 -} - -const depositScriptByteSize = 92 - -var ( - DepositsFormatDescription = `Deposits details should be provided as strings containing: - - bitcoin transaction hash (unprefixed bitcoin transaction hash in reverse (RPC) order), - - bitcoin transaction output index, - - ethereum block number when the deposit was revealed to the chain. -The properties should be separated by semicolons, in the following format: -` + depositsFormatPattern + ` -e.g. bd99d1d0a61fd104925d9b7ac997958aa8af570418b3fde091f7bfc561608865:1:8392394 -` - depositsFormatPattern = "::" - depositsFormatRegexp = regexp.MustCompile(`^([[:xdigit:]]+):(\d+):(\d+)$`) -) - -// DepositSweepDetails contains deposit's data required for sweeping. -type DepositSweepDetails struct { - FundingTxHash bitcoin.Hash - FundingOutputIndex uint32 - RevealBlock uint64 -} - -// ProposeDepositsSweep handles deposit sweep proposal request submission. -func ProposeDepositsSweep( - chain Chain, - btcChain bitcoin.Chain, - walletPublicKeyHash [20]byte, - fee int64, - deposits []*DepositSweepDetails, - dryRun bool, -) error { - if len(deposits) == 0 { - return fmt.Errorf("deposits list is empty") - } - - // Estimate fee if it's missing. - if fee <= 0 { - logger.Infof("estimating sweep transaction fee...") - var err error - _, _, perDepositMaxFee, _, err := chain.GetDepositParameters() - if err != nil { - return fmt.Errorf("cannot get deposit tx max fee: [%w]", err) - } - - estimatedFee, _, err := estimateDepositsSweepFee( - btcChain, - len(deposits), - perDepositMaxFee, - ) - if err != nil { - return fmt.Errorf("cannot estimate sweep transaction fee: [%w]", err) - } - - fee = estimatedFee - } - - logger.Infof("sweep transaction fee: [%d]", fee) - - logger.Infof("preparing a deposit sweep proposal...") - btcTransactions := make([]btcTransaction, len(deposits)) - depositsRevealBlocks := make([]*big.Int, len(deposits)) - for i, deposit := range deposits { - btcTransactions[i] = btcTransaction{ - FundingTxHash: deposit.FundingTxHash, - FundingOutputIndex: deposit.FundingOutputIndex, - } - depositsRevealBlocks[i] = big.NewInt(int64(deposit.RevealBlock)) - } - - proposal := &tbtc.DepositSweepProposal{ - WalletPublicKeyHash: walletPublicKeyHash, - DepositsKeys: btcTransactions, - SweepTxFee: big.NewInt(fee), - DepositsRevealBlocks: depositsRevealBlocks, - } - - logger.Infof("validating the deposit sweep proposal...") - if _, err := tbtc.ValidateDepositSweepProposal( - logger, - proposal, - tbtc.DepositSweepRequiredFundingTxConfirmations, - chain, - btcChain, - ); err != nil { - return fmt.Errorf("failed to verify deposit sweep proposal: %v", err) - } - - if !dryRun { - logger.Infof("submitting the deposit sweep proposal...") - if err := chain.SubmitDepositSweepProposalWithReimbursement(proposal); err != nil { - return fmt.Errorf("failed to submit deposit sweep proposal: %v", err) - } - } - - return nil -} - -// ParseDepositsToSweep decodes a list of deposits details required for sweeping. -func ParseDepositsToSweep(depositsStrings []string) ([]*DepositSweepDetails, error) { - deposits := make([]*DepositSweepDetails, len(depositsStrings)) - - for i, depositString := range depositsStrings { - matched := depositsFormatRegexp.FindStringSubmatch(depositString) - // Check if number of resolved entries match expected number of groups - // for the given regexp. - if len(matched) != 4 { - return nil, fmt.Errorf("failed to parse deposit: [%s]", depositString) - } - - txHash, err := bitcoin.NewHashFromString(matched[1], bitcoin.ReversedByteOrder) - if err != nil { - return nil, fmt.Errorf("invalid bitcoin transaction hash [%s]: %v", matched[1], err) - - } - - outputIndex, err := strconv.ParseInt(matched[2], 10, 32) - if err != nil { - return nil, fmt.Errorf("invalid bitcoin transaction output index [%s]: %v", matched[2], err) - } - - revealBlock, err := strconv.ParseUint(matched[3], 10, 32) - if err != nil { - return nil, fmt.Errorf("invalid reveal block number [%s]: %v", matched[3], err) - } - - deposits[i] = &DepositSweepDetails{ - FundingTxHash: txHash, - FundingOutputIndex: uint32(outputIndex), - RevealBlock: revealBlock, - } - } - - return deposits, nil -} - -// ValidateDepositString validates format of the string containing deposit details. -func ValidateDepositString(depositString string) error { - if !depositsFormatRegexp.MatchString(depositString) { - return fmt.Errorf( - "[%s] doesn't match pattern: %s", - depositString, - depositsFormatPattern, - ) - } - return nil -} - -// EstimateDepositsSweepFee computes the total fee for the Bitcoin deposits -// sweep transaction for the given depositsCount. If the provided depositsCount -// is 0, this function computes the total fee for Bitcoin deposits sweep -// transactions containing a various number of input deposits, from 1 up to the -// maximum count allowed by the WalletCoordinator contract. Computed fees for -// specific deposits counts are printed as table to the standard output, -// for example: -// -// --------------------------------------------- -// deposits count total fee (satoshis) sat/vbyte -// -// 1 201 1 -// 2 292 1 -// 3 384 1 -// -// --------------------------------------------- -// -// While making estimations, this function assumes a sweep transaction -// consists of: -// - 1 P2WPKH input being the current wallet main UTXO. That means the produced -// fees may be overestimated for the very first sweep transaction of -// each wallet. -// - N P2WSH inputs representing the deposits. Worth noting that real -// transactions may contain legacy P2SH deposits as well so produced fees may -// be underestimated in some rare cases. -// - 1 P2WPKH output -// -// If any of the estimated fees exceed the maximum fee allowed by the Bridge -// contract, the maximum fee is returned as result. -func EstimateDepositsSweepFee( - chain Chain, - btcChain bitcoin.Chain, - depositsCount int, -) error { - _, _, perDepositMaxFee, _, err := chain.GetDepositParameters() - if err != nil { - return fmt.Errorf("cannot get deposit tx max fee: [%v]", err) - } - - fees := make(map[int]struct { - totalFee int64 - satPerVByteFee int64 - }) - var depositsCountKeys []int - - if depositsCount > 0 { - depositsCountKeys = append(depositsCountKeys, depositsCount) - } else { - sweepMaxSize, err := chain.GetDepositSweepMaxSize() - if err != nil { - return fmt.Errorf("cannot get sweep max size: [%v]", sweepMaxSize) - } - - for i := 1; i <= int(sweepMaxSize); i++ { - depositsCountKeys = append(depositsCountKeys, i) - } - } - - for _, depositsCountKey := range depositsCountKeys { - totalFee, satPerVByteFee, err := estimateDepositsSweepFee( - btcChain, - depositsCountKey, - perDepositMaxFee, - ) - if err != nil { - return fmt.Errorf( - "cannot estimate fee for deposits count [%v]: [%v]", - depositsCountKey, - err, - ) - } - - fees[depositsCountKey] = struct { - totalFee int64 - satPerVByteFee int64 - }{ - totalFee: totalFee, - satPerVByteFee: satPerVByteFee, - } - } - - err = printDepositsSweepFeeTable(fees) - if err != nil { - return fmt.Errorf("cannot print fees table: [%v]", err) - } - - return nil -} - -func estimateDepositsSweepFee( - btcChain bitcoin.Chain, - depositsCount int, - perDepositMaxFee uint64, -) (int64, int64, error) { - transactionSize, err := bitcoin.NewTransactionSizeEstimator(). - // 1 P2WPKH main UTXO input. - AddPublicKeyHashInputs(1, true). - // depositsCount P2WSH deposit inputs. - AddScriptHashInputs(depositsCount, depositScriptByteSize, true). - // 1 P2WPKH output. - AddPublicKeyHashOutputs(1, true). - VirtualSize() - if err != nil { - return 0, 0, fmt.Errorf("cannot estimate transaction virtual size: [%v]", err) - } - - feeEstimator := bitcoin.NewTransactionFeeEstimator(btcChain) - - totalFee, err := feeEstimator.EstimateFee(transactionSize) - if err != nil { - return 0, 0, fmt.Errorf("cannot estimate transaction fee: [%v]", err) - } - - // Compute the maximum possible total fee for the entire sweep transaction. - totalMaxFee := uint64(depositsCount) * perDepositMaxFee - // Make sure the proposed total fee does not exceed the maximum possible total fee. - totalFee = int64(math.Min(float64(totalFee), float64(totalMaxFee))) - // Compute the actual sat/vbyte fee for informational purposes. - satPerVByteFee := math.Round(float64(totalFee) / float64(transactionSize)) - - return totalFee, int64(satPerVByteFee), nil -} - -func printDepositsSweepFeeTable( - fees map[int]struct { - totalFee int64 - satPerVByteFee int64 - }, -) error { - writer := tabwriter.NewWriter( - os.Stdout, - 2, - 4, - 1, - ' ', - tabwriter.AlignRight, - ) - - _, err := fmt.Fprintf(writer, "deposits count\ttotal fee (satoshis)\tsat/vbyte\t\n") - if err != nil { - return err - } - - var depositsCountKeys []int - for depositsCountKey := range fees { - depositsCountKeys = append(depositsCountKeys, depositsCountKey) - } - - sort.Slice(depositsCountKeys, func(i, j int) bool { - return depositsCountKeys[i] < depositsCountKeys[j] - }) - - for _, depositsCountKey := range depositsCountKeys { - _, err := fmt.Fprintf( - writer, - "%v\t%v\t%v\t\n", - depositsCountKey, - fees[depositsCountKey].totalFee, - fees[depositsCountKey].satPerVByteFee, - ) - if err != nil { - return err - } - } - - if err := writer.Flush(); err != nil { - return fmt.Errorf("failed to flush the writer: %v", err) - } - - return nil -} diff --git a/pkg/coordinator/sweep_test.go b/pkg/coordinator/sweep_test.go deleted file mode 100644 index 756f2ea0d8..0000000000 --- a/pkg/coordinator/sweep_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package coordinator_test - -import ( - "testing" - - "github.com/go-test/deep" - "github.com/ipfs/go-log" - - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/coordinator" - "github.com/keep-network/keep-core/pkg/coordinator/internal/test" - "github.com/keep-network/keep-core/pkg/tbtc" -) - -func TestProposeDepositsSweep(t *testing.T) { - log.SetLogLevel("*", "DEBUG") - - scenarios, err := test.LoadProposeSweepTestScenario() - if err != nil { - t.Fatal(err) - } - - for _, scenario := range scenarios { - t.Run(scenario.Title, func(t *testing.T) { - tbtcChain := newLocalTbtcChain() - btcChain := newLocalBitcoinChain() - - // Chain setup. - tbtcChain.setDepositParameters(0, 0, scenario.DepositTxMaxFee, 0) - - for _, deposit := range scenario.Deposits { - tbtcChain.addPastDepositRevealedEvent( - &tbtc.DepositRevealedEventFilter{ - StartBlock: deposit.RevealBlock, - EndBlock: &deposit.RevealBlock, - WalletPublicKeyHash: [][20]byte{scenario.WalletPublicKeyHash}}, - &tbtc.DepositRevealedEvent{ - WalletPublicKeyHash: scenario.WalletPublicKeyHash, - FundingTxHash: deposit.FundingTxHash, - FundingOutputIndex: deposit.FundingOutputIndex, - }, - ) - btcChain.setTransaction(deposit.FundingTxHash, &bitcoin.Transaction{}) - btcChain.setTransactionConfirmations(deposit.FundingTxHash, tbtc.DepositSweepRequiredFundingTxConfirmations) - } - - tbtcChain.setDepositSweepProposalValidationResult(scenario.ExpectedDepositSweepProposal, nil, true) - btcChain.setEstimateSatPerVByteFee(1, scenario.EstimateSatPerVByteFee) - - // Test execution. - err := coordinator.ProposeDepositsSweep( - tbtcChain, - btcChain, - scenario.WalletPublicKeyHash, - scenario.SweepTxFee, - scenario.DepositsSweepDetails(), - false, - ) - - if err != nil { - t.Fatal(err) - } - - if diff := deep.Equal( - tbtcChain.depositSweepProposals, - []*tbtc.DepositSweepProposal{scenario.ExpectedDepositSweepProposal}, - ); diff != nil { - t.Errorf("invalid deposits: %v", diff) - } - }) - } -} diff --git a/pkg/coordinator/bitcoin_chain_test.go b/pkg/maintainer/wallet/bitcoin_chain_test.go similarity index 71% rename from pkg/coordinator/bitcoin_chain_test.go rename to pkg/maintainer/wallet/bitcoin_chain_test.go index a97494b7e7..2a250a8ed3 100644 --- a/pkg/coordinator/bitcoin_chain_test.go +++ b/pkg/maintainer/wallet/bitcoin_chain_test.go @@ -1,4 +1,4 @@ -package coordinator_test +package wallet import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/keep-network/keep-core/pkg/bitcoin" ) -type localBitcoinChain struct { +type LocalBitcoinChain struct { mutex sync.Mutex transactions map[bitcoin.Hash]*bitcoin.Transaction @@ -15,15 +15,15 @@ type localBitcoinChain struct { satPerVByteFeeEstimation map[uint32]int64 } -func newLocalBitcoinChain() *localBitcoinChain { - return &localBitcoinChain{ +func NewLocalBitcoinChain() *LocalBitcoinChain { + return &LocalBitcoinChain{ transactions: make(map[bitcoin.Hash]*bitcoin.Transaction), transactionsConfirmations: make(map[bitcoin.Hash]uint), satPerVByteFeeEstimation: make(map[uint32]int64), } } -func (lbc *localBitcoinChain) GetTransaction( +func (lbc *LocalBitcoinChain) GetTransaction( transactionHash bitcoin.Hash, ) (*bitcoin.Transaction, error) { lbc.mutex.Lock() @@ -36,7 +36,7 @@ func (lbc *localBitcoinChain) GetTransaction( return transaction, nil } -func (lbc *localBitcoinChain) setTransaction( +func (lbc *LocalBitcoinChain) SetTransaction( transactionHash bitcoin.Hash, transaction *bitcoin.Transaction, ) { @@ -46,7 +46,7 @@ func (lbc *localBitcoinChain) setTransaction( lbc.transactions[transactionHash] = transaction } -func (lbc *localBitcoinChain) GetTransactionConfirmations( +func (lbc *LocalBitcoinChain) GetTransactionConfirmations( transactionHash bitcoin.Hash, ) (uint, error) { lbc.mutex.Lock() @@ -59,7 +59,7 @@ func (lbc *localBitcoinChain) GetTransactionConfirmations( return 0, fmt.Errorf("transaction not found") } -func (lbc *localBitcoinChain) setTransactionConfirmations( +func (lbc *LocalBitcoinChain) SetTransactionConfirmations( transactionHash bitcoin.Hash, confirmations uint, ) { @@ -69,43 +69,43 @@ func (lbc *localBitcoinChain) setTransactionConfirmations( lbc.transactionsConfirmations[transactionHash] = confirmations } -func (lbc *localBitcoinChain) BroadcastTransaction( +func (lbc *LocalBitcoinChain) BroadcastTransaction( transaction *bitcoin.Transaction, ) error { panic("unsupported") } -func (lbc *localBitcoinChain) GetLatestBlockHeight() (uint, error) { +func (lbc *LocalBitcoinChain) GetLatestBlockHeight() (uint, error) { panic("unsupported") } -func (lbc *localBitcoinChain) GetBlockHeader( +func (lbc *LocalBitcoinChain) GetBlockHeader( blockNumber uint, ) (*bitcoin.BlockHeader, error) { panic("unsupported") } -func (lbc *localBitcoinChain) GetTransactionMerkleProof( +func (lbc *LocalBitcoinChain) GetTransactionMerkleProof( transactionHash bitcoin.Hash, blockHeight uint, ) (*bitcoin.TransactionMerkleProof, error) { panic("unsupported") } -func (lbc *localBitcoinChain) GetTransactionsForPublicKeyHash( +func (lbc *LocalBitcoinChain) GetTransactionsForPublicKeyHash( publicKeyHash [20]byte, limit int, ) ([]*bitcoin.Transaction, error) { panic("unsupported") } -func (lbc *localBitcoinChain) GetMempoolForPublicKeyHash( +func (lbc *LocalBitcoinChain) GetMempoolForPublicKeyHash( publicKeyHash [20]byte, ) ([]*bitcoin.Transaction, error) { panic("unsupported") } -func (lbc *localBitcoinChain) EstimateSatPerVByteFee( +func (lbc *LocalBitcoinChain) EstimateSatPerVByteFee( blocks uint32, ) (int64, error) { lbc.mutex.Lock() @@ -114,7 +114,7 @@ func (lbc *localBitcoinChain) EstimateSatPerVByteFee( return lbc.satPerVByteFeeEstimation[blocks], nil } -func (lbc *localBitcoinChain) setEstimateSatPerVByteFee( +func (lbc *LocalBitcoinChain) SetEstimateSatPerVByteFee( blocks uint32, fee int64, ) { diff --git a/pkg/maintainer/wallet/chain.go b/pkg/maintainer/wallet/chain.go index 474adcca6d..9e218b3569 100644 --- a/pkg/maintainer/wallet/chain.go +++ b/pkg/maintainer/wallet/chain.go @@ -1,17 +1,134 @@ package wallet import ( + "github.com/keep-network/keep-core/pkg/bitcoin" + "math/big" "time" - "github.com/keep-network/keep-core/pkg/coordinator" "github.com/keep-network/keep-core/pkg/tbtc" ) -// Chain represents the interface that the wallet maintainer module expects to interact -// with the anchoring blockchain on. +// Chain represents the interface that the wallet maintainer module expects +// to interact with the anchoring blockchain on. type Chain interface { - // coordinator.Chain is an interface required by the coordinator package. - coordinator.Chain + // GetDepositRequest gets the on-chain deposit request for the given + // funding transaction hash and output index.The returned values represent: + // - deposit request which is non-nil only when the deposit request was + // found, + // - boolean value which is true if the deposit request was found, false + // otherwise, + // - error which is non-nil only when the function execution failed. It will + // be nil if the deposit request was not found, but the function execution + // succeeded. + GetDepositRequest( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, + ) (*tbtc.DepositChainRequest, bool, error) + + // PastNewWalletRegisteredEvents fetches past new wallet registered 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. + PastNewWalletRegisteredEvents( + filter *tbtc.NewWalletRegisteredEventFilter, + ) ([]*tbtc.NewWalletRegisteredEvent, error) + + // BuildDepositKey calculates a deposit key for the given funding transaction + // which is a unique identifier for a deposit on-chain. + BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int + + // GetDepositParameters gets the current value of parameters relevant + // for the depositing process. + GetDepositParameters() ( + dustThreshold uint64, + treasuryFeeDivisor uint64, + txMaxFee uint64, + revealAheadPeriod uint32, + err error, + ) + + // SubmitDepositSweepProposalWithReimbursement submits a deposit sweep + // proposal to the chain. It reimburses the gas cost to the caller. + SubmitDepositSweepProposalWithReimbursement( + proposal *tbtc.DepositSweepProposal, + ) error + + // PastRedemptionRequestedEvents fetches past redemption requested 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. + PastRedemptionRequestedEvents( + filter *tbtc.RedemptionRequestedEventFilter, + ) ([]*tbtc.RedemptionRequestedEvent, error) + + // BuildRedemptionKey calculates a redemption key for the given redemption + // request which is an identifier for a redemption at the given time + // on-chain. + BuildRedemptionKey( + walletPublicKeyHash [20]byte, + redeemerOutputScript bitcoin.Script, + ) (*big.Int, error) + + // GetRedemptionParameters gets the current value of parameters relevant + // for the redemption process. + GetRedemptionParameters() ( + dustThreshold uint64, + treasuryFeeDivisor uint64, + txMaxFee uint64, + txMaxTotalFee uint64, + timeout uint32, + timeoutSlashingAmount *big.Int, + timeoutNotifierRewardMultiplier uint32, + err error, + ) + + // SubmitRedemptionProposalWithReimbursement submits a redemption proposal + // to the chain. It reimburses the gas cost to the caller. + SubmitRedemptionProposalWithReimbursement( + proposal *tbtc.RedemptionProposal, + ) error + + // GetRedemptionMaxSize gets the maximum number of redemption requests that + // can be a part of a redemption sweep proposal. + GetRedemptionMaxSize() (uint16, error) + + // GetRedemptionRequestMinAge get the minimum time that must elapse since + // the redemption request creation before a request becomes eligible for + // a processing. + GetRedemptionRequestMinAge() (uint32, error) + + // PastDepositRevealedEvents fetches past deposit reveal 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. + PastDepositRevealedEvents( + filter *tbtc.DepositRevealedEventFilter, + ) ([]*tbtc.DepositRevealedEvent, error) + + // GetPendingRedemptionRequest gets the on-chain pending redemption request + // for the given wallet public key hash and redeemer output script. + // Returns an error if the request was not found. + GetPendingRedemptionRequest( + walletPublicKeyHash [20]byte, + redeemerOutputScript bitcoin.Script, + ) (*tbtc.RedemptionRequest, error) + + // ValidateDepositSweepProposal validates the given deposit sweep proposal + // against the chain. It requires some additional data about the deposits + // that must be fetched externally. Returns an error if the proposal is + // not valid or nil otherwise. + ValidateDepositSweepProposal( + proposal *tbtc.DepositSweepProposal, + depositsExtraInfo []struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, + ) error + + // ValidateRedemptionProposal validates the given redemption proposal + // against the chain. Returns an error if the proposal is not valid or + // nil otherwise. + ValidateRedemptionProposal(proposal *tbtc.RedemptionProposal) error // GetDepositSweepMaxSize gets the maximum number of deposits that can // be part of a deposit sweep proposal. diff --git a/pkg/maintainer/wallet/chain_test.go b/pkg/maintainer/wallet/chain_test.go index 542cff994b..97ffa8440d 100644 --- a/pkg/maintainer/wallet/chain_test.go +++ b/pkg/maintainer/wallet/chain_test.go @@ -1,13 +1,16 @@ package wallet import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/hex" "fmt" + "github.com/keep-network/keep-core/pkg/bitcoin" "math/big" "sync" "time" - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/coordinator" "github.com/keep-network/keep-core/pkg/tbtc" ) @@ -16,72 +19,290 @@ type walletLock struct { walletAction tbtc.WalletActionType } -type localChain struct { +type depositParameters = struct { + dustThreshold uint64 + treasuryFeeDivisor uint64 + txMaxFee uint64 + revealAheadPeriod uint32 +} + +type LocalChain struct { mutex sync.Mutex - walletLocks map[[20]byte]*walletLock + depositRequests map[[32]byte]*tbtc.DepositChainRequest + pastDepositRevealedEvents map[[32]byte][]*tbtc.DepositRevealedEvent + pastNewWalletRegisteredEvents map[[32]byte][]*tbtc.NewWalletRegisteredEvent + depositParameters depositParameters + depositSweepProposalValidations map[[32]byte]bool + depositSweepProposals []*tbtc.DepositSweepProposal + walletLocks map[[20]byte]*walletLock } -func newLocalChain() *localChain { - return &localChain{ - walletLocks: make(map[[20]byte]*walletLock), +func NewLocalChain() *LocalChain { + return &LocalChain{ + depositRequests: make(map[[32]byte]*tbtc.DepositChainRequest), + pastDepositRevealedEvents: make(map[[32]byte][]*tbtc.DepositRevealedEvent), + pastNewWalletRegisteredEvents: make(map[[32]byte][]*tbtc.NewWalletRegisteredEvent), + depositSweepProposalValidations: make(map[[32]byte]bool), + walletLocks: make(map[[20]byte]*walletLock), } } -func (lc *localChain) PastDepositRevealedEvents( +func (lc *LocalChain) DepositSweepProposals() []*tbtc.DepositSweepProposal { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.depositSweepProposals +} + +func (lc *LocalChain) PastDepositRevealedEvents( filter *tbtc.DepositRevealedEventFilter, ) ([]*tbtc.DepositRevealedEvent, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastDepositRevealedEventsKey(filter) + if err != nil { + return nil, err + } + + events, ok := lc.pastDepositRevealedEvents[eventsKey] + if !ok { + return nil, fmt.Errorf("no events for given filter") + } + + return events, nil +} + +func (lc *LocalChain) AddPastDepositRevealedEvent( + filter *tbtc.DepositRevealedEventFilter, + event *tbtc.DepositRevealedEvent, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastDepositRevealedEventsKey(filter) + if err != nil { + return err + } + + lc.pastDepositRevealedEvents[eventsKey] = append( + lc.pastDepositRevealedEvents[eventsKey], + event, + ) + + return nil +} + +func buildPastDepositRevealedEventsKey( + filter *tbtc.DepositRevealedEventFilter, +) ([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 _, depositor := range filter.Depositor { + depositorBytes, err := hex.DecodeString(depositor.String()) + if err != nil { + return [32]byte{}, err + } + + buffer.Write(depositorBytes) + } + + for _, walletPublicKeyHash := range filter.WalletPublicKeyHash { + buffer.Write(walletPublicKeyHash[:]) + } + + return sha256.Sum256(buffer.Bytes()), nil } -func (lc *localChain) GetDepositRequest( +func (lc *LocalChain) GetDepositRequest( fundingTxHash bitcoin.Hash, fundingOutputIndex uint32, ) (*tbtc.DepositChainRequest, bool, error) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + requestKey := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) + + request, ok := lc.depositRequests[requestKey] + if !ok { + return nil, false, nil + } + + return request, true, nil } -func (lc *localChain) PastNewWalletRegisteredEvents( - filter *coordinator.NewWalletRegisteredEventFilter, -) ([]*coordinator.NewWalletRegisteredEvent, error) { - panic("unsupported") +func (lc *LocalChain) SetDepositRequest( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, + request *tbtc.DepositChainRequest, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + requestKey := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) + + lc.depositRequests[requestKey] = request +} + +func (lc *LocalChain) PastNewWalletRegisteredEvents( + filter *tbtc.NewWalletRegisteredEventFilter, +) ([]*tbtc.NewWalletRegisteredEvent, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastNewWalletRegisteredEventsKey(filter) + if err != nil { + return nil, err + } + + events, ok := lc.pastNewWalletRegisteredEvents[eventsKey] + if !ok { + return nil, fmt.Errorf("no events for given filter") + } + + return events, nil } -func (lc *localChain) PastRedemptionRequestedEvents( +func (lc *LocalChain) AddPastNewWalletRegisteredEvent( + filter *tbtc.NewWalletRegisteredEventFilter, + event *tbtc.NewWalletRegisteredEvent, +) error { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + eventsKey, err := buildPastNewWalletRegisteredEventsKey(filter) + if err != nil { + return err + } + + if _, ok := lc.pastNewWalletRegisteredEvents[eventsKey]; !ok { + lc.pastNewWalletRegisteredEvents[eventsKey] = []*tbtc.NewWalletRegisteredEvent{} + } + + lc.pastNewWalletRegisteredEvents[eventsKey] = append( + lc.pastNewWalletRegisteredEvents[eventsKey], + event, + ) + + return nil +} + +func buildPastNewWalletRegisteredEventsKey( + filter *tbtc.NewWalletRegisteredEventFilter, +) ([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 _, ecdsaWalletID := range filter.EcdsaWalletID { + buffer.Write(ecdsaWalletID[:]) + } + + for _, walletPublicKeyHash := range filter.WalletPublicKeyHash { + buffer.Write(walletPublicKeyHash[:]) + } + + return sha256.Sum256(buffer.Bytes()), nil +} + +func (lc *LocalChain) PastRedemptionRequestedEvents( filter *tbtc.RedemptionRequestedEventFilter, ) ([]*tbtc.RedemptionRequestedEvent, error) { panic("unsupported") } -func (lc *localChain) BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int { - panic("unsupported") +func (lc *LocalChain) BuildDepositKey(fundingTxHash bitcoin.Hash, fundingOutputIndex uint32) *big.Int { + depositKeyBytes := buildDepositRequestKey(fundingTxHash, fundingOutputIndex) + + return new(big.Int).SetBytes(depositKeyBytes[:]) +} + +func buildDepositRequestKey( + fundingTxHash bitcoin.Hash, + fundingOutputIndex uint32, +) [32]byte { + fundingOutputIndexBytes := make([]byte, 4) + binary.BigEndian.PutUint32(fundingOutputIndexBytes, fundingOutputIndex) + + return sha256.Sum256(append(fundingTxHash[:], fundingOutputIndexBytes...)) } -func (lc *localChain) BuildRedemptionKey( +func (lc *LocalChain) BuildRedemptionKey( walletPublicKeyHash [20]byte, redeemerOutputScript bitcoin.Script, ) (*big.Int, error) { panic("unsupported") } -func (lc *localChain) GetPendingRedemptionRequest( +func (lc *LocalChain) GetDepositParameters() ( + dustThreshold uint64, + treasuryFeeDivisor uint64, + txMaxFee uint64, + revealAheadPeriod uint32, + err error, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + return lc.depositParameters.dustThreshold, + lc.depositParameters.treasuryFeeDivisor, + lc.depositParameters.txMaxFee, + lc.depositParameters.revealAheadPeriod, + nil +} + +func (lc *LocalChain) GetPendingRedemptionRequest( walletPublicKeyHash [20]byte, redeemerOutputScript bitcoin.Script, ) (*tbtc.RedemptionRequest, error) { panic("unsupported") } -func (lc *localChain) GetDepositParameters() ( +func (lc *LocalChain) SetDepositParameters( dustThreshold uint64, treasuryFeeDivisor uint64, txMaxFee uint64, revealAheadPeriod uint32, - err error, ) { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.depositParameters = depositParameters{ + dustThreshold: dustThreshold, + treasuryFeeDivisor: treasuryFeeDivisor, + txMaxFee: txMaxFee, + revealAheadPeriod: revealAheadPeriod, + } } -func (lc *localChain) GetRedemptionParameters() ( +func (lc *LocalChain) GetRedemptionParameters() ( dustThreshold uint64, treasuryFeeDivisor uint64, txMaxFee uint64, @@ -94,82 +315,145 @@ func (lc *localChain) GetRedemptionParameters() ( panic("unsupported") } -func (lc *localChain) GetWalletLock( - walletPublicKeyHash [20]byte, -) (time.Time, tbtc.WalletActionType, error) { +func (lc *LocalChain) ValidateDepositSweepProposal( + proposal *tbtc.DepositSweepProposal, + depositsExtraInfo []struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, +) error { lc.mutex.Lock() defer lc.mutex.Unlock() - walletLock, ok := lc.walletLocks[walletPublicKeyHash] + key, err := buildDepositSweepProposalValidationKey(proposal) + if err != nil { + return err + } + + result, ok := lc.depositSweepProposalValidations[key] if !ok { - return time.Time{}, tbtc.Noop, fmt.Errorf("no lock configured for given wallet") + return fmt.Errorf("validation result unknown") } - return walletLock.lockExpiration, walletLock.walletAction, nil + if !result { + return fmt.Errorf("validation failed") + } + + return nil } -func (lc *localChain) setWalletLock( - walletPublicKeyHash [20]byte, - lockExpiration time.Time, - walletAction tbtc.WalletActionType, -) { +func (lc *LocalChain) SetDepositSweepProposalValidationResult( + proposal *tbtc.DepositSweepProposal, + depositsExtraInfo []struct { + *tbtc.Deposit + FundingTx *bitcoin.Transaction + }, + result bool, +) error { lc.mutex.Lock() defer lc.mutex.Unlock() - lc.walletLocks[walletPublicKeyHash] = &walletLock{ - lockExpiration: lockExpiration, - walletAction: walletAction, + key, err := buildDepositSweepProposalValidationKey(proposal) + if err != nil { + return err } + + lc.depositSweepProposalValidations[key] = result + + return nil } -func (lc *localChain) resetWalletLock( - walletPublicKeyHash [20]byte, -) { - lc.mutex.Lock() - defer lc.mutex.Unlock() +func buildDepositSweepProposalValidationKey( + proposal *tbtc.DepositSweepProposal, +) ([32]byte, error) { + var buffer bytes.Buffer - lc.walletLocks[walletPublicKeyHash] = &walletLock{ - lockExpiration: time.Unix(0, 0), - walletAction: tbtc.Noop, + buffer.Write(proposal.WalletPublicKeyHash[:]) + + for _, deposit := range proposal.DepositsKeys { + buffer.Write(deposit.FundingTxHash[:]) + + fundingOutputIndex := make([]byte, 4) + binary.BigEndian.PutUint32(fundingOutputIndex, deposit.FundingOutputIndex) + buffer.Write(fundingOutputIndex) } + + buffer.Write(proposal.SweepTxFee.Bytes()) + + return sha256.Sum256(buffer.Bytes()), nil } -func (lc *localChain) ValidateDepositSweepProposal( +func (lc *LocalChain) SubmitDepositSweepProposalWithReimbursement( proposal *tbtc.DepositSweepProposal, - depositsExtraInfo []struct { - *tbtc.Deposit - FundingTx *bitcoin.Transaction - }, ) error { - panic("unsupported") + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.depositSweepProposals = append(lc.depositSweepProposals, proposal) + + return nil } -func (lc *localChain) SubmitDepositSweepProposalWithReimbursement( - proposal *tbtc.DepositSweepProposal, +func (lc *LocalChain) SubmitRedemptionProposalWithReimbursement( + proposal *tbtc.RedemptionProposal, ) error { panic("unsupported") } -func (lc *localChain) SubmitRedemptionProposalWithReimbursement( +func (lc *LocalChain) ValidateRedemptionProposal( proposal *tbtc.RedemptionProposal, ) error { panic("unsupported") } -func (lc *localChain) GetDepositSweepMaxSize() (uint16, error) { +func (lc *LocalChain) GetRedemptionMaxSize() (uint16, error) { panic("unsupported") } -func (lc *localChain) ValidateRedemptionProposal( - proposal *tbtc.RedemptionProposal, -) error { +func (lc *LocalChain) GetRedemptionRequestMinAge() (uint32, error) { panic("unsupported") } -func (lc *localChain) GetRedemptionMaxSize() (uint16, error) { +func (lc *LocalChain) GetDepositSweepMaxSize() (uint16, error) { panic("unsupported") } -func (lc *localChain) GetRedemptionRequestMinAge() (uint32, error) { - panic("unsupported") +func (lc *LocalChain) GetWalletLock( + walletPublicKeyHash [20]byte, +) (time.Time, tbtc.WalletActionType, error) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + walletLock, ok := lc.walletLocks[walletPublicKeyHash] + if !ok { + return time.Time{}, tbtc.Noop, fmt.Errorf("no lock configured for given wallet") + } + + return walletLock.lockExpiration, walletLock.walletAction, nil +} + +func (lc *LocalChain) SetWalletLock( + walletPublicKeyHash [20]byte, + lockExpiration time.Time, + walletAction tbtc.WalletActionType, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.walletLocks[walletPublicKeyHash] = &walletLock{ + lockExpiration: lockExpiration, + walletAction: walletAction, + } +} + +func (lc *LocalChain) ResetWalletLock( + walletPublicKeyHash [20]byte, +) { + lc.mutex.Lock() + defer lc.mutex.Unlock() + + lc.walletLocks[walletPublicKeyHash] = &walletLock{ + lockExpiration: time.Unix(0, 0), + walletAction: tbtc.Noop, + } } diff --git a/pkg/maintainer/wallet/config.go b/pkg/maintainer/wallet/config.go index 54960be32e..711fbcdbde 100644 --- a/pkg/maintainer/wallet/config.go +++ b/pkg/maintainer/wallet/config.go @@ -3,13 +3,13 @@ package wallet import "time" const ( - DefaultRedemptionInterval = 3 * time.Hour - DefaultSweepInterval = 48 * time.Hour + DefaultRedemptionInterval = 3 * time.Hour + DefaultDepositSweepInterval = 48 * time.Hour ) // Config holds configurable properties. type Config struct { - Enabled bool - RedemptionInterval time.Duration - SweepInterval time.Duration + Enabled bool + RedemptionInterval time.Duration + DepositSweepInterval time.Duration } diff --git a/pkg/maintainer/wallet/deposit_sweep.go b/pkg/maintainer/wallet/deposit_sweep.go new file mode 100644 index 0000000000..365db73b4a --- /dev/null +++ b/pkg/maintainer/wallet/deposit_sweep.go @@ -0,0 +1,515 @@ +package wallet + +import ( + "context" + "fmt" + "math" + "math/big" + "sort" + + "github.com/keep-network/keep-core/internal/hexutils" + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" +) + +const depositScriptByteSize = 92 + +func (wm *walletMaintainer) runDepositSweepTask(ctx context.Context) error { + depositSweepMaxSize, err := wm.chain.GetDepositSweepMaxSize() + if err != nil { + return fmt.Errorf("failed to get deposit sweep max size: [%w]", err) + } + + walletPublicKeyHash, deposits, err := FindDepositsToSweep( + wm.chain, + wm.btcChain, + [20]byte{}, + depositSweepMaxSize, + ) + if err != nil { + return fmt.Errorf("failed to prepare deposits sweep proposal: [%w]", err) + } + + if len(deposits) == 0 { + logger.Info("no deposits to sweep") + return nil + } + + return wm.runIfWalletUnlocked( + ctx, + walletPublicKeyHash, + tbtc.DepositSweep, + func() error { + return ProposeDepositsSweep( + wm.chain, + wm.btcChain, + walletPublicKeyHash, + 0, + deposits, + false, + ) + }, + ) +} + +// DepositReference holds some data allowing to identify and refer to a deposit. +type DepositReference struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + RevealBlock uint64 +} + +// Deposit holds some detailed data about a deposit. +type Deposit struct { + DepositReference + + WalletPublicKeyHash [20]byte + DepositKey string + IsSwept bool + AmountBtc float64 + Confirmations uint +} + +// FindDeposits finds deposits according to the given criteria. +func FindDeposits( + chain Chain, + btcChain bitcoin.Chain, + walletPublicKeyHash [20]byte, + maxNumberOfDeposits int, + skipSwept bool, + skipUnconfirmed bool, +) ([]*Deposit, error) { + logger.Infof("reading revealed deposits from chain...") + + filter := &tbtc.DepositRevealedEventFilter{} + if walletPublicKeyHash != [20]byte{} { + filter.WalletPublicKeyHash = [][20]byte{walletPublicKeyHash} + } + + depositRevealedEvents, err := chain.PastDepositRevealedEvents(filter) + if err != nil { + return []*Deposit{}, fmt.Errorf( + "failed to get past deposit revealed events: [%w]", + err, + ) + } + + logger.Infof("found %d DepositRevealed events", len(depositRevealedEvents)) + + // Take the oldest first + sort.SliceStable(depositRevealedEvents, func(i, j int) bool { + return depositRevealedEvents[i].BlockNumber < depositRevealedEvents[j].BlockNumber + }) + + logger.Infof("getting deposits details...") + + resultSliceCapacity := len(depositRevealedEvents) + if maxNumberOfDeposits > 0 { + resultSliceCapacity = maxNumberOfDeposits + } + + result := make([]*Deposit, 0, resultSliceCapacity) + for i, event := range depositRevealedEvents { + if len(result) == cap(result) { + break + } + + logger.Debugf("getting details of deposit %d/%d", i+1, len(depositRevealedEvents)) + + depositKey := chain.BuildDepositKey(event.FundingTxHash, event.FundingOutputIndex) + + depositRequest, found, err := chain.GetDepositRequest( + event.FundingTxHash, + event.FundingOutputIndex, + ) + if err != nil { + return result, fmt.Errorf( + "failed to get deposit request: [%w]", + err, + ) + } + + if !found { + return nil, fmt.Errorf( + "no deposit request for key [0x%x]", + depositKey.Text(16), + ) + } + + isSwept := depositRequest.SweptAt.Unix() != 0 + if skipSwept && isSwept { + logger.Debugf("deposit %d/%d is already swept", i+1, len(depositRevealedEvents)) + continue + } + + confirmations, err := btcChain.GetTransactionConfirmations(event.FundingTxHash) + if err != nil { + logger.Errorf( + "failed to get bitcoin transaction confirmations: [%v]", + err, + ) + } + + if skipUnconfirmed && confirmations < tbtc.DepositSweepRequiredFundingTxConfirmations { + logger.Debugf( + "deposit %d/%d funding transaction doesn't have enough confirmations: %d/%d", + i+1, len(depositRevealedEvents), + confirmations, tbtc.DepositSweepRequiredFundingTxConfirmations) + continue + } + + result = append( + result, + &Deposit{ + DepositReference: DepositReference{ + FundingTxHash: event.FundingTxHash, + FundingOutputIndex: event.FundingOutputIndex, + RevealBlock: event.BlockNumber, + }, + WalletPublicKeyHash: event.WalletPublicKeyHash, + DepositKey: hexutils.Encode(depositKey.Bytes()), + IsSwept: isSwept, + AmountBtc: convertSatToBtc(float64(depositRequest.Amount)), + Confirmations: confirmations, + }, + ) + } + + return result, nil +} + +// FindDepositsToSweep finds deposits that can be swept. +// If a wallet public key hash is provided, it will find unswept deposits for the +// given wallet. If a wallet public key hash is nil, it will check all wallets +// starting from the oldest one to find a first wallet containing unswept deposits +// and return those deposits. +// maxNumberOfDeposits is used as a ceiling for the number of deposits in the +// result. If number of discovered deposits meets the maxNumberOfDeposits the +// function will stop fetching more deposits. +// This function will return a wallet public key hash and a list of deposits from +// the wallet that can be swept. +// Deposits with insufficient number of funding transaction confirmations will +// not be taken into consideration for sweeping. +// The result will not mix deposits for different wallets. +// +// TODO: Cache immutable data +func FindDepositsToSweep( + chain Chain, + btcChain bitcoin.Chain, + walletPublicKeyHash [20]byte, + maxNumberOfDeposits uint16, +) ([20]byte, []*DepositReference, error) { + logger.Infof("deposit sweep max size: %d", maxNumberOfDeposits) + + getDepositsToSweepFromWallet := func(walletToSweep [20]byte) ([]*Deposit, error) { + unsweptDeposits, err := FindDeposits( + chain, + btcChain, + walletToSweep, + int(maxNumberOfDeposits), + true, + true, + ) + if err != nil { + return nil, + fmt.Errorf( + "failed to get deposits for [%s] wallet: [%w]", + walletToSweep, + err, + ) + } + return unsweptDeposits, nil + } + + var depositsToSweep []*Deposit + // If walletPublicKeyHash is not provided we need to find a wallet that has + // unswept deposits. + if walletPublicKeyHash == [20]byte{} { + walletRegisteredEvents, err := chain.PastNewWalletRegisteredEvents(nil) + if err != nil { + return [20]byte{}, nil, fmt.Errorf("failed to get registered wallets: [%w]", err) + } + + // Take the oldest first + sort.SliceStable(walletRegisteredEvents, func(i, j int) bool { + return walletRegisteredEvents[i].BlockNumber < walletRegisteredEvents[j].BlockNumber + }) + + sweepingWallets := walletRegisteredEvents + // Only two the most recently created wallets are sweeping. + if len(walletRegisteredEvents) >= 2 { + sweepingWallets = walletRegisteredEvents[len(walletRegisteredEvents)-2:] + } + + for _, registeredWallet := range sweepingWallets { + logger.Infof( + "fetching deposits from wallet [%s]...", + hexutils.Encode(registeredWallet.WalletPublicKeyHash[:]), + ) + + unsweptDeposits, err := getDepositsToSweepFromWallet( + registeredWallet.WalletPublicKeyHash, + ) + if err != nil { + return [20]byte{}, nil, err + } + + // Check if there are any unswept deposits in this wallet. If so + // sweep this wallet and don't check the other wallet. + if len(unsweptDeposits) > 0 { + walletPublicKeyHash = registeredWallet.WalletPublicKeyHash + depositsToSweep = unsweptDeposits + break + } + } + } else { + logger.Infof( + "fetching deposits from wallet [%s]...", + hexutils.Encode(walletPublicKeyHash[:]), + ) + unsweptDeposits, err := getDepositsToSweepFromWallet( + walletPublicKeyHash, + ) + if err != nil { + return [20]byte{}, nil, err + } + depositsToSweep = unsweptDeposits + } + + if len(depositsToSweep) == 0 { + return [20]byte{}, nil, nil + } + + logger.Infof( + "found [%d] deposits to sweep for wallet [%s]", + len(depositsToSweep), + hexutils.Encode(walletPublicKeyHash[:]), + ) + + for i, deposit := range depositsToSweep { + logger.Infof( + "deposit [%d/%d] - %s", + i+1, + len(depositsToSweep), + fmt.Sprintf( + "depositKey: [%s], reveal block: [%d], funding transaction: [%s], output index: [%d]", + deposit.DepositKey, + deposit.RevealBlock, + deposit.FundingTxHash.Hex(bitcoin.ReversedByteOrder), + deposit.FundingOutputIndex, + )) + } + + depositsRefs := make([]*DepositReference, len(depositsToSweep)) + for i, deposit := range depositsToSweep { + depositsRefs[i] = &DepositReference{ + FundingTxHash: deposit.FundingTxHash, + FundingOutputIndex: deposit.FundingOutputIndex, + RevealBlock: deposit.RevealBlock, + } + } + + return walletPublicKeyHash, depositsRefs, nil +} + +// ProposeDepositsSweep handles deposit sweep proposal request submission. +func ProposeDepositsSweep( + chain Chain, + btcChain bitcoin.Chain, + walletPublicKeyHash [20]byte, + fee int64, + deposits []*DepositReference, + dryRun bool, +) error { + if len(deposits) == 0 { + return fmt.Errorf("deposits list is empty") + } + + // Estimate fee if it's missing. + if fee <= 0 { + logger.Infof("estimating sweep transaction fee...") + var err error + _, _, perDepositMaxFee, _, err := chain.GetDepositParameters() + if err != nil { + return fmt.Errorf("cannot get deposit tx max fee: [%w]", err) + } + + estimatedFee, _, err := estimateDepositsSweepFee( + btcChain, + len(deposits), + perDepositMaxFee, + ) + if err != nil { + return fmt.Errorf("cannot estimate sweep transaction fee: [%w]", err) + } + + fee = estimatedFee + } + + logger.Infof("sweep transaction fee: [%d]", fee) + + logger.Infof("preparing a deposit sweep proposal...") + + depositsKeys := make([]struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + }, len(deposits)) + + depositsRevealBlocks := make([]*big.Int, len(deposits)) + + for i, deposit := range deposits { + depositsKeys[i] = struct { + FundingTxHash bitcoin.Hash + FundingOutputIndex uint32 + }{ + FundingTxHash: deposit.FundingTxHash, + FundingOutputIndex: deposit.FundingOutputIndex, + } + depositsRevealBlocks[i] = big.NewInt(int64(deposit.RevealBlock)) + } + + proposal := &tbtc.DepositSweepProposal{ + WalletPublicKeyHash: walletPublicKeyHash, + DepositsKeys: depositsKeys, + SweepTxFee: big.NewInt(fee), + DepositsRevealBlocks: depositsRevealBlocks, + } + + logger.Infof("validating the deposit sweep proposal...") + if _, err := tbtc.ValidateDepositSweepProposal( + logger, + proposal, + tbtc.DepositSweepRequiredFundingTxConfirmations, + chain, + btcChain, + ); err != nil { + return fmt.Errorf("failed to verify deposit sweep proposal: %v", err) + } + + if !dryRun { + logger.Infof("submitting the deposit sweep proposal...") + if err := chain.SubmitDepositSweepProposalWithReimbursement(proposal); err != nil { + return fmt.Errorf("failed to submit deposit sweep proposal: %v", err) + } + } + + return nil +} + +// EstimateDepositsSweepFee computes the total fee for the Bitcoin deposits +// sweep transaction for the given depositsCount. If the provided depositsCount +// is 0, this function computes the total fee for Bitcoin deposits sweep +// transactions containing a various number of input deposits, from 1 up to the +// maximum count allowed by the WalletCoordinator contract. Computed fees for +// specific deposits counts are returned as a map. +// +// While making estimations, this function assumes a sweep transaction +// consists of: +// - 1 P2WPKH input being the current wallet main UTXO. That means the produced +// fees may be overestimated for the very first sweep transaction of +// each wallet. +// - N P2WSH inputs representing the deposits. Worth noting that real +// transactions may contain legacy P2SH deposits as well so produced fees may +// be underestimated in some rare cases. +// - 1 P2WPKH output +// +// If any of the estimated fees exceed the maximum fee allowed by the Bridge +// contract, the maximum fee is returned as result. +func EstimateDepositsSweepFee( + chain Chain, + btcChain bitcoin.Chain, + depositsCount int, +) ( + map[int]struct { + TotalFee int64 + SatPerVByteFee int64 + }, + error, +) { + _, _, perDepositMaxFee, _, err := chain.GetDepositParameters() + if err != nil { + return nil, fmt.Errorf("cannot get deposit tx max fee: [%v]", err) + } + + fees := make(map[int]struct { + TotalFee int64 + SatPerVByteFee int64 + }) + var depositsCountKeys []int + + if depositsCount > 0 { + depositsCountKeys = append(depositsCountKeys, depositsCount) + } else { + sweepMaxSize, err := chain.GetDepositSweepMaxSize() + if err != nil { + return nil, fmt.Errorf("cannot get sweep max size: [%v]", sweepMaxSize) + } + + for i := 1; i <= int(sweepMaxSize); i++ { + depositsCountKeys = append(depositsCountKeys, i) + } + } + + for _, depositsCountKey := range depositsCountKeys { + totalFee, satPerVByteFee, err := estimateDepositsSweepFee( + btcChain, + depositsCountKey, + perDepositMaxFee, + ) + if err != nil { + return nil, fmt.Errorf( + "cannot estimate fee for deposits count [%v]: [%v]", + depositsCountKey, + err, + ) + } + + fees[depositsCountKey] = struct { + TotalFee int64 + SatPerVByteFee int64 + }{ + TotalFee: totalFee, + SatPerVByteFee: satPerVByteFee, + } + } + + return fees, nil +} + +func estimateDepositsSweepFee( + btcChain bitcoin.Chain, + depositsCount int, + perDepositMaxFee uint64, +) (int64, int64, error) { + transactionSize, err := bitcoin.NewTransactionSizeEstimator(). + // 1 P2WPKH main UTXO input. + AddPublicKeyHashInputs(1, true). + // depositsCount P2WSH deposit inputs. + AddScriptHashInputs(depositsCount, depositScriptByteSize, true). + // 1 P2WPKH output. + AddPublicKeyHashOutputs(1, true). + VirtualSize() + if err != nil { + return 0, 0, fmt.Errorf("cannot estimate transaction virtual size: [%v]", err) + } + + feeEstimator := bitcoin.NewTransactionFeeEstimator(btcChain) + + totalFee, err := feeEstimator.EstimateFee(transactionSize) + if err != nil { + return 0, 0, fmt.Errorf("cannot estimate transaction fee: [%v]", err) + } + + // Compute the maximum possible total fee for the entire sweep transaction. + totalMaxFee := uint64(depositsCount) * perDepositMaxFee + // Make sure the proposed total fee does not exceed the maximum possible total fee. + totalFee = int64(math.Min(float64(totalFee), float64(totalMaxFee))) + // Compute the actual sat/vbyte fee for informational purposes. + satPerVByteFee := math.Round(float64(totalFee) / float64(transactionSize)) + + return totalFee, int64(satPerVByteFee), nil +} + +func convertSatToBtc(sats float64) float64 { + return sats / float64(100000000) +} diff --git a/pkg/maintainer/wallet/deposit_sweep_test.go b/pkg/maintainer/wallet/deposit_sweep_test.go new file mode 100644 index 0000000000..4464eb016b --- /dev/null +++ b/pkg/maintainer/wallet/deposit_sweep_test.go @@ -0,0 +1,171 @@ +package wallet_test + +import ( + "testing" + + "github.com/go-test/deep" + "github.com/ipfs/go-log" + "github.com/keep-network/keep-core/internal/hexutils" + "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" +) + +func TestFindDepositsToSweep(t *testing.T) { + err := log.SetLogLevel("*", "DEBUG") + if err != nil { + t.Fatal(err) + } + + scenarios, err := test.LoadFindDepositsToSweepTestScenario() + if err != nil { + t.Fatal(err) + } + + for _, scenario := range scenarios { + t.Run(scenario.Title, func(t *testing.T) { + tbtcChain := walletmtr.NewLocalChain() + btcChain := walletmtr.NewLocalBitcoinChain() + + expectedWallet := scenario.ExpectedWalletPublicKeyHash + + // Chain setup. + for _, wallet := range scenario.Wallets { + err := tbtcChain.AddPastNewWalletRegisteredEvent( + nil, + &tbtc.NewWalletRegisteredEvent{ + WalletPublicKeyHash: wallet.WalletPublicKeyHash, + BlockNumber: wallet.RegistrationBlockNumber, + }, + ) + if err != nil { + t.Fatal(err) + } + } + + for _, deposit := range scenario.Deposits { + tbtcChain.SetDepositRequest( + deposit.FundingTxHash, + deposit.FundingOutputIndex, + &tbtc.DepositChainRequest{SweptAt: deposit.SweptAt}, + ) + btcChain.SetTransactionConfirmations( + deposit.FundingTxHash, + deposit.FundingTxConfirmations, + ) + + err := tbtcChain.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{WalletPublicKeyHash: [][20]byte{deposit.WalletPublicKeyHash}}, + &tbtc.DepositRevealedEvent{ + BlockNumber: deposit.RevealBlockNumber, + WalletPublicKeyHash: deposit.WalletPublicKeyHash, + FundingTxHash: deposit.FundingTxHash, + FundingOutputIndex: deposit.FundingOutputIndex, + }, + ) + if err != nil { + t.Fatal(err) + } + } + + // Test execution. + actualWallet, actualDeposits, err := walletmtr.FindDepositsToSweep( + tbtcChain, + btcChain, + scenario.WalletPublicKeyHash, + scenario.MaxNumberOfDeposits, + ) + + if err != nil { + t.Fatal(err) + } + + if actualWallet != expectedWallet { + t.Errorf( + "invalid wallet public key hash\nexpected: %s\nactual: %s", + hexutils.Encode(expectedWallet[:]), + hexutils.Encode(actualWallet[:]), + ) + } + + if diff := deep.Equal(actualDeposits, scenario.ExpectedUnsweptDeposits); diff != nil { + t.Errorf("invalid deposits: %v", diff) + } + }) + } +} + +func TestProposeDepositsSweep(t *testing.T) { + err := log.SetLogLevel("*", "DEBUG") + if err != nil { + t.Fatal(err) + } + + scenarios, err := test.LoadProposeSweepTestScenario() + if err != nil { + t.Fatal(err) + } + + for _, scenario := range scenarios { + t.Run(scenario.Title, func(t *testing.T) { + tbtcChain := walletmtr.NewLocalChain() + btcChain := walletmtr.NewLocalBitcoinChain() + + // Chain setup. + tbtcChain.SetDepositParameters(0, 0, scenario.DepositTxMaxFee, 0) + + for _, deposit := range scenario.Deposits { + err := tbtcChain.AddPastDepositRevealedEvent( + &tbtc.DepositRevealedEventFilter{ + StartBlock: deposit.RevealBlock, + EndBlock: &deposit.RevealBlock, + WalletPublicKeyHash: [][20]byte{scenario.WalletPublicKeyHash}, + }, + &tbtc.DepositRevealedEvent{ + WalletPublicKeyHash: scenario.WalletPublicKeyHash, + FundingTxHash: deposit.FundingTxHash, + FundingOutputIndex: deposit.FundingOutputIndex, + }, + ) + if err != nil { + t.Fatal(err) + } + + btcChain.SetTransaction(deposit.FundingTxHash, &bitcoin.Transaction{}) + btcChain.SetTransactionConfirmations(deposit.FundingTxHash, tbtc.DepositSweepRequiredFundingTxConfirmations) + } + + err := tbtcChain.SetDepositSweepProposalValidationResult( + scenario.ExpectedDepositSweepProposal, + nil, + true, + ) + if err != nil { + t.Fatal(err) + } + + btcChain.SetEstimateSatPerVByteFee(1, scenario.EstimateSatPerVByteFee) + + // Test execution. + err = walletmtr.ProposeDepositsSweep( + tbtcChain, + btcChain, + scenario.WalletPublicKeyHash, + scenario.SweepTxFee, + scenario.DepositsReferences(), + false, + ) + if err != nil { + t.Fatal(err) + } + + if diff := deep.Equal( + tbtcChain.DepositSweepProposals(), + []*tbtc.DepositSweepProposal{scenario.ExpectedDepositSweepProposal}, + ); diff != nil { + t.Errorf("invalid deposits: %v", diff) + } + }) + } +} diff --git a/pkg/coordinator/internal/test/marshaling.go b/pkg/maintainer/wallet/internal/test/marshaling.go similarity index 96% rename from pkg/coordinator/internal/test/marshaling.go rename to pkg/maintainer/wallet/internal/test/marshaling.go index 5e3e2e8dca..c36d7d5d30 100644 --- a/pkg/coordinator/internal/test/marshaling.go +++ b/pkg/maintainer/wallet/internal/test/marshaling.go @@ -3,12 +3,12 @@ package test import ( "encoding/json" "fmt" + walletmtr "github.com/keep-network/keep-core/pkg/maintainer/wallet" "math/big" "time" "github.com/keep-network/keep-core/internal/hexutils" "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/coordinator" "github.com/keep-network/keep-core/pkg/tbtc" ) @@ -57,11 +57,11 @@ func (dsts *FindDepositsToSweepTestScenario) UnmarshalJSON(data []byte) error { dsts.MaxNumberOfDeposits = unmarshaled.MaxNumberOfDeposits // Unmarshal wallets. - for _, wallet := range unmarshaled.Wallets { + for _, uw := range unmarshaled.Wallets { w := new(Wallet) - copy(w.WalletPublicKeyHash[:], hexToSlice(wallet.WalletPublicKeyHash)) - w.RegistrationBlockNumber = wallet.RegistrationBlockNumber + copy(w.WalletPublicKeyHash[:], hexToSlice(uw.WalletPublicKeyHash)) + w.RegistrationBlockNumber = uw.RegistrationBlockNumber dsts.Wallets = append(dsts.Wallets, w) } @@ -97,7 +97,7 @@ func (dsts *FindDepositsToSweepTestScenario) UnmarshalJSON(data []byte) error { // Unmarshal expected unswept deposits. for i, deposit := range unmarshaled.ExpectedUnsweptDeposits { - ud := new(coordinator.DepositSweepDetails) + ud := new(walletmtr.DepositReference) fundingTxHash, err := bitcoin.NewHashFromString(deposit.FundingTxHash, bitcoin.ReversedByteOrder) if err != nil { diff --git a/pkg/coordinator/internal/test/testdata/find_deposits_scenario_0.json b/pkg/maintainer/wallet/internal/test/testdata/find_deposits_scenario_0.json similarity index 100% rename from pkg/coordinator/internal/test/testdata/find_deposits_scenario_0.json rename to pkg/maintainer/wallet/internal/test/testdata/find_deposits_scenario_0.json diff --git a/pkg/coordinator/internal/test/testdata/find_deposits_scenario_1.json b/pkg/maintainer/wallet/internal/test/testdata/find_deposits_scenario_1.json similarity index 100% rename from pkg/coordinator/internal/test/testdata/find_deposits_scenario_1.json rename to pkg/maintainer/wallet/internal/test/testdata/find_deposits_scenario_1.json diff --git a/pkg/coordinator/internal/test/testdata/find_deposits_scenario_2.json b/pkg/maintainer/wallet/internal/test/testdata/find_deposits_scenario_2.json similarity index 100% rename from pkg/coordinator/internal/test/testdata/find_deposits_scenario_2.json rename to pkg/maintainer/wallet/internal/test/testdata/find_deposits_scenario_2.json diff --git a/pkg/coordinator/internal/test/testdata/find_deposits_scenario_3.json b/pkg/maintainer/wallet/internal/test/testdata/find_deposits_scenario_3.json similarity index 100% rename from pkg/coordinator/internal/test/testdata/find_deposits_scenario_3.json rename to pkg/maintainer/wallet/internal/test/testdata/find_deposits_scenario_3.json diff --git a/pkg/coordinator/internal/test/testdata/propose_sweep_scenario_0.json b/pkg/maintainer/wallet/internal/test/testdata/propose_sweep_scenario_0.json similarity index 100% rename from pkg/coordinator/internal/test/testdata/propose_sweep_scenario_0.json rename to pkg/maintainer/wallet/internal/test/testdata/propose_sweep_scenario_0.json diff --git a/pkg/coordinator/internal/test/testdata/propose_sweep_scenario_1.json b/pkg/maintainer/wallet/internal/test/testdata/propose_sweep_scenario_1.json similarity index 100% rename from pkg/coordinator/internal/test/testdata/propose_sweep_scenario_1.json rename to pkg/maintainer/wallet/internal/test/testdata/propose_sweep_scenario_1.json diff --git a/pkg/coordinator/internal/test/testdata/propose_sweep_scenario_2.json b/pkg/maintainer/wallet/internal/test/testdata/propose_sweep_scenario_2.json similarity index 100% rename from pkg/coordinator/internal/test/testdata/propose_sweep_scenario_2.json rename to pkg/maintainer/wallet/internal/test/testdata/propose_sweep_scenario_2.json diff --git a/pkg/coordinator/internal/test/coordinatortest.go b/pkg/maintainer/wallet/internal/test/wallettest.go similarity index 91% rename from pkg/coordinator/internal/test/coordinatortest.go rename to pkg/maintainer/wallet/internal/test/wallettest.go index 268a5e1d86..44d5d5de5c 100644 --- a/pkg/coordinator/internal/test/coordinatortest.go +++ b/pkg/maintainer/wallet/internal/test/wallettest.go @@ -3,6 +3,7 @@ package test import ( "encoding/json" "fmt" + walletmtr "github.com/keep-network/keep-core/pkg/maintainer/wallet" "io/fs" "io/ioutil" "path/filepath" @@ -11,7 +12,6 @@ import ( "time" "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/coordinator" "github.com/keep-network/keep-core/pkg/tbtc" ) @@ -50,7 +50,7 @@ type FindDepositsToSweepTestScenario struct { Deposits []*Deposit ExpectedWalletPublicKeyHash [20]byte - ExpectedUnsweptDeposits []*coordinator.DepositSweepDetails + ExpectedUnsweptDeposits []*walletmtr.DepositReference SweepTxFee int64 EstimateSatPerVByteFee int64 @@ -97,7 +97,8 @@ func LoadFindDepositsToSweepTestScenario() ([]*FindDepositsToSweepTestScenario, } type ProposeSweepDepositsData struct { - coordinator.DepositSweepDetails + walletmtr.DepositReference + Transaction *bitcoin.Transaction FundingTxConfirmations uint } @@ -114,10 +115,10 @@ type ProposeSweepTestScenario struct { ExpectedDepositSweepProposal *tbtc.DepositSweepProposal } -func (p *ProposeSweepTestScenario) DepositsSweepDetails() []*coordinator.DepositSweepDetails { - result := make([]*coordinator.DepositSweepDetails, len(p.Deposits)) - for i, d := range p.Deposits { - result[i] = &coordinator.DepositSweepDetails{ +func (psts *ProposeSweepTestScenario) DepositsReferences() []*walletmtr.DepositReference { + result := make([]*walletmtr.DepositReference, len(psts.Deposits)) + for i, d := range psts.Deposits { + result[i] = &walletmtr.DepositReference{ FundingTxHash: d.FundingTxHash, FundingOutputIndex: d.FundingOutputIndex, RevealBlock: d.RevealBlock, diff --git a/pkg/coordinator/redemptions.go b/pkg/maintainer/wallet/redemptions.go similarity index 87% rename from pkg/coordinator/redemptions.go rename to pkg/maintainer/wallet/redemptions.go index 590468d9af..92ba9eab00 100644 --- a/pkg/coordinator/redemptions.go +++ b/pkg/maintainer/wallet/redemptions.go @@ -1,23 +1,22 @@ -package coordinator +package wallet import ( "errors" "fmt" + "github.com/keep-network/keep-core/internal/hexutils" + "github.com/keep-network/keep-core/pkg/bitcoin" + "github.com/keep-network/keep-core/pkg/tbtc" "math/big" "sort" "time" - - "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/tbtc" - - "github.com/keep-network/keep-core/internal/hexutils" ) -type redemptionEntry struct { - walletPublicKeyHash [20]byte - redemptionKey string - redeemerOutputScript bitcoin.Script - requestedAt time.Time +// RedemptionRequest represents a redemption request. +type RedemptionRequest struct { + WalletPublicKeyHash [20]byte + RedemptionKey string + RedeemerOutputScript bitcoin.Script + RequestedAt time.Time } // FindPendingRedemptions finds pending redemptions requests. @@ -44,7 +43,7 @@ func FindPendingRedemptions( ) } - getPendingRedemptionsFromWallet := func(wallet [20]byte) ([]*redemptionEntry, error) { + getPendingRedemptionsFromWallet := func(wallet [20]byte) ([]*RedemptionRequest, error) { pendingRedemptions, err := getPendingRedemptions( chain, wallet, @@ -63,7 +62,7 @@ func FindPendingRedemptions( return pendingRedemptions, nil } - var redemptionsToPropose []*redemptionEntry + var redemptionsToPropose []*RedemptionRequest // If walletPublicKeyHash is not provided we need to find a wallet that has // pending redemptions. if walletPublicKeyHash == [20]byte{} { @@ -129,11 +128,11 @@ func FindPendingRedemptions( len(redemptionsToPropose), fmt.Sprintf( "redemptionKey: [%s], requested at: [%s]", - redemption.redemptionKey, - redemption.requestedAt, + redemption.RedemptionKey, + redemption.RequestedAt, )) - redeemersOutputScripts[i] = redemption.redeemerOutputScript + redeemersOutputScripts[i] = redemption.RedeemerOutputScript } return walletPublicKeyHash, redeemersOutputScripts, nil @@ -211,7 +210,7 @@ func getPendingRedemptions( maxNumberOfRedemptions int, redemptionRequestTimeout uint32, redemptionRequestMinAge uint32, -) ([]*redemptionEntry, error) { +) ([]*RedemptionRequest, error) { logger.Infof("reading pending redemptions from chain...") filter := &tbtc.RedemptionRequestedEventFilter{} @@ -248,7 +247,7 @@ func getPendingRedemptions( logger.Infof("checking pending redemptions details...") - pendingRedemptions := make([]*redemptionEntry, 0) + pendingRedemptions := make([]*RedemptionRequest, 0) redemptionRequestedLoop: for i, event := range redemptionsRequested { @@ -286,11 +285,11 @@ redemptionRequestedLoop: return nil, fmt.Errorf("failed to build redemption key: [%v]", err) } - pendingRedemptions = append(pendingRedemptions, &redemptionEntry{ - walletPublicKeyHash: event.WalletPublicKeyHash, - redemptionKey: hexutils.Encode(redemptionKey.Bytes()), - redeemerOutputScript: event.RedeemerOutputScript, - requestedAt: pendingRedemption.RequestedAt, + pendingRedemptions = append(pendingRedemptions, &RedemptionRequest{ + WalletPublicKeyHash: event.WalletPublicKeyHash, + RedemptionKey: hexutils.Encode(redemptionKey.Bytes()), + RedeemerOutputScript: event.RedeemerOutputScript, + RequestedAt: pendingRedemption.RequestedAt, }) } @@ -301,17 +300,17 @@ redemptionRequestedLoop: // Sort the pending redemptions. sort.SliceStable(pendingRedemptions, func(i, j int) bool { - return pendingRedemptions[i].requestedAt.Before(pendingRedemptions[j].requestedAt) + return pendingRedemptions[i].RequestedAt.Before(pendingRedemptions[j].RequestedAt) }) - result := make([]*redemptionEntry, 0, resultSliceCapacity) + result := make([]*RedemptionRequest, 0, resultSliceCapacity) for i, pendingRedemption := range pendingRedemptions { if len(result) == cap(result) { break } // Check if timeout passed for the redemption request. - if pendingRedemption.requestedAt.Before(redemptionRequestsRangeStartTimestamp) { + if pendingRedemption.RequestedAt.Before(redemptionRequestsRangeStartTimestamp) { logger.Infof( "redemption request %d/%d has already timed out", i+1, @@ -321,7 +320,7 @@ redemptionRequestedLoop: } // Check if enough time elapsed since the redemption request. - if pendingRedemption.requestedAt.After(redemptionRequestsRangeEndTimestamp) { + if pendingRedemption.RequestedAt.After(redemptionRequestsRangeEndTimestamp) { logger.Infof( "redemption request %d/%d is not old enough", i+1, @@ -336,6 +335,8 @@ redemptionRequestedLoop: return result, nil } +// EstimateRedemptionFee estimates fee for the redemption transaction that pays +// the provided redeemers output scripts. func EstimateRedemptionFee( btcChain bitcoin.Chain, redeemersOutputScripts []bitcoin.Script, diff --git a/pkg/coordinator/redemptions_test.go b/pkg/maintainer/wallet/redemptions_test.go similarity index 80% rename from pkg/coordinator/redemptions_test.go rename to pkg/maintainer/wallet/redemptions_test.go index 990845c610..f305515e92 100644 --- a/pkg/coordinator/redemptions_test.go +++ b/pkg/maintainer/wallet/redemptions_test.go @@ -1,10 +1,10 @@ -package coordinator_test +package wallet_test import ( "encoding/hex" "github.com/keep-network/keep-core/internal/testutils" "github.com/keep-network/keep-core/pkg/bitcoin" - "github.com/keep-network/keep-core/pkg/coordinator" + walletmtr "github.com/keep-network/keep-core/pkg/maintainer/wallet" "testing" ) @@ -19,8 +19,8 @@ func TestEstimateRedemptionFee(t *testing.T) { return bytes } - btcChain := newLocalBitcoinChain() - btcChain.setEstimateSatPerVByteFee(1, 16) + btcChain := walletmtr.NewLocalBitcoinChain() + btcChain.SetEstimateSatPerVByteFee(1, 16) redeemersOutputScripts := []bitcoin.Script{ fromHex("76a9142cd680318747b720d67bf4246eb7403b476adb3488ac"), // P2PKH @@ -29,7 +29,7 @@ func TestEstimateRedemptionFee(t *testing.T) { fromHex("0020ef0b4d985752aa5ef6243e4c6f6bebc2a007e7d671ef27d4b1d0db8dcc93bc1c"), // P2WSH } - actualFee, err := coordinator.EstimateRedemptionFee(btcChain, redeemersOutputScripts) + actualFee, err := walletmtr.EstimateRedemptionFee(btcChain, redeemersOutputScripts) if err != nil { t.Fatal(err) } diff --git a/pkg/maintainer/wallet/sweep.go b/pkg/maintainer/wallet/sweep.go deleted file mode 100644 index a4e5d665a1..0000000000 --- a/pkg/maintainer/wallet/sweep.go +++ /dev/null @@ -1,47 +0,0 @@ -package wallet - -import ( - "context" - "fmt" - - "github.com/keep-network/keep-core/pkg/coordinator" - "github.com/keep-network/keep-core/pkg/tbtc" -) - -func (wm *walletMaintainer) runSweepTask(ctx context.Context) error { - depositSweepMaxSize, err := wm.chain.GetDepositSweepMaxSize() - if err != nil { - return fmt.Errorf("failed to get deposit sweep max size: [%w]", err) - } - - walletPublicKeyHash, deposits, err := coordinator.FindDepositsToSweep( - wm.chain, - wm.btcChain, - [20]byte{}, - depositSweepMaxSize, - ) - if err != nil { - return fmt.Errorf("failed to prepare deposits sweep proposal: [%w]", err) - } - - if len(deposits) == 0 { - logger.Info("no deposits to sweep") - return nil - } - - return wm.runIfWalletUnlocked( - ctx, - walletPublicKeyHash, - tbtc.DepositSweep, - func() error { - return coordinator.ProposeDepositsSweep( - wm.chain, - wm.btcChain, - walletPublicKeyHash, - 0, - deposits, - false, - ) - }, - ) -} diff --git a/pkg/maintainer/wallet/wallet.go b/pkg/maintainer/wallet/wallet.go index fb512a41c3..7e70a5fb46 100644 --- a/pkg/maintainer/wallet/wallet.go +++ b/pkg/maintainer/wallet/wallet.go @@ -31,8 +31,8 @@ func Initialize( if config.RedemptionInterval == 0 { config.RedemptionInterval = DefaultRedemptionInterval } - if config.SweepInterval == 0 { - config.SweepInterval = DefaultSweepInterval + if config.DepositSweepInterval == 0 { + config.DepositSweepInterval = DefaultDepositSweepInterval } wm := &walletMaintainer{ @@ -51,16 +51,16 @@ func (wm *walletMaintainer) startControlLoop(ctx context.Context) { defer logger.Info("stopping wallet coordination maintainer") initialRedemptionDelay := 5 * time.Second - initialSweepDelay := 60 * time.Second + initialDepositSweepDelay := 60 * time.Second redemptionTicker := time.NewTicker(initialRedemptionDelay) defer redemptionTicker.Stop() - sweepTicker := time.NewTicker(initialSweepDelay) - defer sweepTicker.Stop() + depositSweepTicker := time.NewTicker(initialDepositSweepDelay) + defer depositSweepTicker.Stop() logger.Infof("waiting [%s] until redemption task execution", initialRedemptionDelay) - logger.Infof("waiting [%s] until sweep task execution", initialSweepDelay) + logger.Infof("waiting [%s] until deposit sweep task execution", initialDepositSweepDelay) for { select { @@ -74,18 +74,24 @@ func (wm *walletMaintainer) startControlLoop(ctx context.Context) { // TODO: Implement - logger.Infof("redemption task run completed; next run in [%s]", wm.config.RedemptionInterval) - case <-sweepTicker.C: + logger.Infof( + "redemption task run completed; next run in [%s]", + wm.config.RedemptionInterval, + ) + case <-depositSweepTicker.C: // Set the ticker to the expected interval. - sweepTicker.Reset(wm.config.SweepInterval) + depositSweepTicker.Reset(wm.config.DepositSweepInterval) - logger.Info("starting sweep task execution...") + logger.Info("starting deposit sweep task execution...") - if err := wm.runSweepTask(ctx); err != nil { - logger.Errorf("failed to run sweep task: [%v]", err) + if err := wm.runDepositSweepTask(ctx); err != nil { + logger.Errorf("failed to run deposit sweep task: [%v]", err) } - logger.Infof("sweep task run completed; next run in [%s]", wm.config.SweepInterval) + logger.Infof( + "deposit sweep task run completed; next run in [%s]", + wm.config.DepositSweepInterval, + ) } } } diff --git a/pkg/maintainer/wallet/wallet_test.go b/pkg/maintainer/wallet/wallet_test.go index ca15bb84cd..5a2adc8f18 100644 --- a/pkg/maintainer/wallet/wallet_test.go +++ b/pkg/maintainer/wallet/wallet_test.go @@ -11,13 +11,13 @@ import ( ) func TestRunIfWalletUnlocked_WhenLocked(t *testing.T) { - localChain := newLocalChain() + localChain := NewLocalChain() walletPublicKeyHash := [20]byte{1} lockExpiration := time.Now().Add(500 * time.Millisecond) - localChain.setWalletLock( + localChain.SetWalletLock( walletPublicKeyHash, lockExpiration, tbtc.Heartbeat, @@ -55,11 +55,11 @@ func TestRunIfWalletUnlocked_WhenUnlocked(t *testing.T) { for testName, test := range tests { t.Run(testName, func(t *testing.T) { - localChain := newLocalChain() + localChain := NewLocalChain() walletPublicKeyHash := [20]byte{2} - localChain.resetWalletLock(walletPublicKeyHash) + localChain.ResetWalletLock(walletPublicKeyHash) wasCalled := make(chan bool, 1) runFunc := func() error { diff --git a/pkg/tbtc/chain.go b/pkg/tbtc/chain.go index 61fb54a12d..45c2a7b5be 100644 --- a/pkg/tbtc/chain.go +++ b/pkg/tbtc/chain.go @@ -219,6 +219,21 @@ type BridgeChain interface { ) (*RedemptionRequest, error) } +// NewWalletRegisteredEvent represents a new wallet registered event. +type NewWalletRegisteredEvent struct { + EcdsaWalletID [32]byte + WalletPublicKeyHash [20]byte + BlockNumber uint64 +} + +// NewWalletRegisteredEventFilter is a component allowing to filter NewWalletRegisteredEvent. +type NewWalletRegisteredEventFilter struct { + StartBlock uint64 + EndBlock *uint64 + EcdsaWalletID [][32]byte + WalletPublicKeyHash [][20]byte +} + // HeartbeatRequestedEvent represents a Bridge heartbeat request event. type HeartbeatRequestedEvent struct { WalletPublicKey []byte diff --git a/test/config.json b/test/config.json index a9c4fbe5c5..368bdb0f68 100644 --- a/test/config.json +++ b/test/config.json @@ -47,7 +47,7 @@ "WalletCoordination": { "Enabled": true, "RedemptionInterval": "13h", - "SweepInterval": "64h" + "DepositSweepInterval": "64h" }, "Spv": { "Enabled": true diff --git a/test/config.toml b/test/config.toml index b37fb2888a..efa4c2d2cd 100644 --- a/test/config.toml +++ b/test/config.toml @@ -42,7 +42,7 @@ DisableProxy = true [maintainer.WalletCoordination] Enabled = true RedemptionInterval = "13h" -SweepInterval = "64h" +DepositSweepInterval = "64h" [maintainer.Spv] Enabled = true diff --git a/test/config.yaml b/test/config.yaml index 0c688852d0..2777accd65 100644 --- a/test/config.yaml +++ b/test/config.yaml @@ -37,7 +37,7 @@ Maintainer: WalletCoordination: Enabled: true RedemptionInterval: "13h" - SweepInterval: "64h" + DepositSweepInterval: "64h" Spv: Enabled: true Developer: