diff --git a/chain/chainservice.go b/chain/chainservice.go index cd3c4b5aa4..08df57e31d 100644 --- a/chain/chainservice.go +++ b/chain/chainservice.go @@ -5,6 +5,7 @@ import ( "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil" "github.com/ltcsuite/ltcd/ltcutil/gcs" + "github.com/ltcsuite/ltcd/ltcutil/mweb" "github.com/ltcsuite/ltcd/wire" "github.com/ltcsuite/neutrino" "github.com/ltcsuite/neutrino/banman" @@ -22,6 +23,7 @@ type NeutrinoChainService interface { GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, error) IsCurrent() bool SendTransaction(*wire.MsgTx) error + MarkAsConfirmed(chainhash.Hash) GetCFilter(chainhash.Hash, wire.FilterType, ...neutrino.QueryOption) (*gcs.Filter, error) GetUtxo(...neutrino.RescanOption) (*neutrino.SpendReport, error) @@ -31,6 +33,11 @@ type NeutrinoChainService interface { AddBytesSent(uint64) AddBytesReceived(uint64) NetTotals() (uint64, uint64) + RegisterMempoolCallback(func(*ltcutil.Tx)) + NotifyMempoolReceived([]ltcutil.Address) + RegisterMwebUtxosCallback(func(*mweb.Leafset, []*wire.MwebNetUtxo)) + NotifyAddedMwebUtxos(*mweb.Leafset) error + MwebUtxoExists(*chainhash.Hash) bool UpdatePeerHeights(*chainhash.Hash, int32, *neutrino.ServerPeer) ChainParams() chaincfg.Params Stop() error diff --git a/chain/interface.go b/chain/interface.go index cbba083a8d..b347941353 100644 --- a/chain/interface.go +++ b/chain/interface.go @@ -5,6 +5,7 @@ import ( "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil" + "github.com/ltcsuite/ltcd/ltcutil/mweb" "github.com/ltcsuite/ltcd/rpcclient" "github.com/ltcsuite/ltcd/wire" "github.com/ltcsuite/ltcwallet/waddrmgr" @@ -108,6 +109,12 @@ type ( Block *wtxmgr.BlockMeta // nil if unmined } + // MwebUtxos is a notification of new MWEB utxos. + MwebUtxos struct { + Leafset *mweb.Leafset + Utxos []*wire.MwebNetUtxo + } + // RescanProgress is a notification describing the current status // of an in-progress rescan. RescanProgress struct { diff --git a/chain/mocks_test.go b/chain/mocks_test.go index 8c4d915345..e542b1a2e9 100644 --- a/chain/mocks_test.go +++ b/chain/mocks_test.go @@ -8,6 +8,7 @@ import ( "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil" "github.com/ltcsuite/ltcd/ltcutil/gcs" + "github.com/ltcsuite/ltcd/ltcutil/mweb" "github.com/ltcsuite/ltcd/rpcclient" "github.com/ltcsuite/ltcd/wire" "github.com/ltcsuite/neutrino" @@ -104,6 +105,10 @@ func (m *mockChainService) SendTransaction(*wire.MsgTx) error { return errNotImplemented } +func (m *mockChainService) MarkAsConfirmed(chainhash.Hash) { + panic(errNotImplemented) +} + func (m *mockChainService) GetCFilter(chainhash.Hash, wire.FilterType, ...neutrino.QueryOption) (*gcs.Filter, error) { @@ -140,6 +145,24 @@ func (m *mockChainService) NetTotals() (uint64, uint64) { panic(errNotImplemented) } +func (m *mockChainService) RegisterMempoolCallback(func(*ltcutil.Tx)) { +} + +func (m *mockChainService) NotifyMempoolReceived([]ltcutil.Address) { +} + +func (m *mockChainService) RegisterMwebUtxosCallback( + func(*mweb.Leafset, []*wire.MwebNetUtxo)) { +} + +func (m *mockChainService) NotifyAddedMwebUtxos(*mweb.Leafset) error { + return errNotImplemented +} + +func (m *mockChainService) MwebUtxoExists(*chainhash.Hash) bool { + panic(errNotImplemented) +} + func (m *mockChainService) UpdatePeerHeights(*chainhash.Hash, int32, *neutrino.ServerPeer, ) { diff --git a/chain/neutrino.go b/chain/neutrino.go index f99af4848c..2154e81f5a 100644 --- a/chain/neutrino.go +++ b/chain/neutrino.go @@ -9,8 +9,8 @@ import ( "github.com/ltcsuite/ltcd/chaincfg" "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil" - "github.com/ltcsuite/ltcd/ltcutil/gcs" "github.com/ltcsuite/ltcd/ltcutil/gcs/builder" + "github.com/ltcsuite/ltcd/ltcutil/mweb" "github.com/ltcsuite/ltcd/rpcclient" "github.com/ltcsuite/ltcd/txscript" "github.com/ltcsuite/ltcd/wire" @@ -241,12 +241,8 @@ func (s *NeutrinoClient) FilterBlocks( // the filter returns a positive match, the full block is then requested // and scanned for addresses using the block filterer. for i, blk := range req.Blocks { - // TODO(wilmer): Investigate why polling it still necessary - // here. While testing, I ran into a few instances where the - // filter was not retrieved, leading to a panic. This should not - // happen in most cases thanks to the query logic revamp within - // Neutrino, but it seems there's still an uncovered edge case. - filter, err := s.pollCFilter(&blk.Hash) + filter, err := s.CS.GetCFilter(blk.Hash, + wire.GCSFilterRegular, neutrino.OptimisticBatch()) if err != nil { return nil, err } @@ -343,36 +339,6 @@ func buildFilterBlocksWatchList(req *FilterBlocksRequest) ([][]byte, error) { return watchList, nil } -// pollCFilter attempts to fetch a CFilter from the neutrino client. This is -// used to get around the fact that the filter headers may lag behind the -// highest known block header. -func (s *NeutrinoClient) pollCFilter(hash *chainhash.Hash) (*gcs.Filter, error) { - var ( - filter *gcs.Filter - err error - count int - ) - - const maxFilterRetries = 50 - for count < maxFilterRetries { - if count > 0 { - time.Sleep(100 * time.Millisecond) - } - - filter, err = s.CS.GetCFilter( - *hash, wire.GCSFilterRegular, neutrino.OptimisticBatch(), - ) - if err != nil { - count++ - continue - } - - return filter, nil - } - - return nil, err -} - // Rescan replicates the RPC client's Rescan command. func (s *NeutrinoClient) Rescan(startHash *chainhash.Hash, addrs []ltcutil.Address, outPoints map[wire.OutPoint]ltcutil.Address) error { @@ -382,6 +348,9 @@ func (s *NeutrinoClient) Rescan(startHash *chainhash.Hash, addrs []ltcutil.Addre defer s.rescanMtx.Unlock() s.clientMtx.Lock() + + s.CS.NotifyMempoolReceived(addrs) + if !s.started { s.clientMtx.Unlock() return fmt.Errorf("can't do a rescan when the chain client " + @@ -477,6 +446,10 @@ func (s *NeutrinoClient) Rescan(startHash *chainhash.Hash, addrs []ltcutil.Addre // NotifyBlocks replicates the RPC client's NotifyBlocks command. func (s *NeutrinoClient) NotifyBlocks() error { s.clientMtx.Lock() + + s.CS.RegisterMempoolCallback(s.onRecvTx) + s.CS.RegisterMwebUtxosCallback(s.onMwebUtxos) + // If we're scanning, we're already notifying on blocks. Otherwise, // start a rescan without watching any addresses. if !s.scanning { @@ -495,6 +468,8 @@ func (s *NeutrinoClient) NotifyReceived(addrs []ltcutil.Address) error { s.clientMtx.Lock() + s.CS.NotifyMempoolReceived(addrs) + // If we have a rescan running, we just need to add the appropriate // addresses to the watch list. if s.scanning { @@ -797,3 +772,25 @@ out: close(s.dequeueNotification) s.wg.Done() } + +func (s *NeutrinoClient) onRecvTx(tx *ltcutil.Tx) { + rec, err := wtxmgr.NewTxRecordFromMsgTx(tx.MsgTx(), time.Now()) + if err != nil { + log.Errorf("Cannot create transaction record for relevant "+ + "tx: %v", err) + return + } + select { + case s.enqueueNotification <- RelevantTx{rec, nil}: + case <-s.quit: + } +} + +func (s *NeutrinoClient) onMwebUtxos( + leafset *mweb.Leafset, utxos []*wire.MwebNetUtxo) { + + select { + case s.enqueueNotification <- MwebUtxos{leafset, utxos}: + case <-s.quit: + } +} diff --git a/cmd/sweepaccount/main.go b/cmd/sweepaccount/main.go index 2b7029b818..05ee1f1ec3 100644 --- a/cmd/sweepaccount/main.go +++ b/cmd/sweepaccount/main.go @@ -18,6 +18,7 @@ import ( "github.com/ltcsuite/ltcd/wire" "github.com/ltcsuite/ltcwallet/internal/cfgutil" "github.com/ltcsuite/ltcwallet/netparams" + "github.com/ltcsuite/ltcwallet/waddrmgr" "github.com/ltcsuite/ltcwallet/wallet/txauthor" "github.com/ltcsuite/ltcwallet/wallet/txrules" "github.com/ltcsuite/ltcwallet/wallet/txsizes" @@ -181,8 +182,9 @@ func makeInputSource(outputs []btcjson.ListUnspentResult) txauthor.InputSource { sourceErr = noInputValue{} } - return func(ltcutil.Amount) (ltcutil.Amount, []*wire.TxIn, []ltcutil.Amount, [][]byte, error) { - return totalInputValue, inputs, inputValues, nil, sourceErr + return func(ltcutil.Amount) (ltcutil.Amount, []*wire.TxIn, + []ltcutil.Amount, [][]byte, []*wire.MwebOutput, error) { + return totalInputValue, inputs, inputValues, nil, nil, sourceErr } } @@ -193,7 +195,7 @@ func makeDestinationScriptSource(rpcClient *rpcclient.Client, accountName string // GetNewAddress always returns a P2PKH address since it assumes // BIP-0044. - newChangeScript := func() ([]byte, error) { + newChangeScript := func(*waddrmgr.KeyScope) ([]byte, error) { destinationAddress, err := rpcClient.GetNewAddress(accountName) if err != nil { return nil, err diff --git a/go.mod b/go.mod index c857d65872..47f42d1599 100644 --- a/go.mod +++ b/go.mod @@ -5,69 +5,30 @@ require ( github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 - github.com/jessevdk/go-flags v1.4.0 + github.com/jessevdk/go-flags v1.5.0 github.com/jrick/logrotate v1.0.0 github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf - github.com/ltcsuite/lnd/ticker v1.1.0 - github.com/ltcsuite/lnd/tlv v1.1.1 - github.com/ltcsuite/ltcd v0.23.5 - github.com/ltcsuite/ltcd/btcec/v2 v2.3.2 - github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 - github.com/ltcsuite/ltcd/ltcutil v1.1.3 - github.com/ltcsuite/ltcd/ltcutil/psbt v1.1.8 - github.com/ltcsuite/ltcwallet/wallet/txauthor v1.3.2 - github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 - github.com/ltcsuite/ltcwallet/wallet/txsizes v1.2.3 - github.com/ltcsuite/ltcwallet/walletdb v1.4.0 - github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0 - github.com/ltcsuite/neutrino v0.16.0 - github.com/ltcsuite/neutrino/cache v1.1.1 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.3 golang.org/x/crypto v0.7.0 golang.org/x/net v0.10.0 golang.org/x/term v0.8.0 - google.golang.org/grpc v1.53.0 - google.golang.org/protobuf v1.28.1 + google.golang.org/grpc v1.56.3 + google.golang.org/protobuf v1.30.0 + lukechampine.com/blake3 v1.2.1 ) require ( - github.com/aead/siphash v1.0.1 // indirect - github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect - github.com/decred/dcrd/lru v1.1.1 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/kkdai/bstream v1.0.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect - github.com/ltcsuite/lnd/clock v1.1.0 // indirect - github.com/ltcsuite/lnd/queue v1.1.0 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stretchr/objx v0.5.0 // indirect - go.etcd.io/bbolt v1.3.7 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.9.0 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.2.1 // indirect ) go 1.18 - -replace github.com/ltcsuite/ltcwallet/walletdb => ./walletdb - -replace github.com/ltcsuite/neutrino => ../neutrino - -replace github.com/ltcsuite/neutrino/cache => ../neutrino/cache - -replace github.com/ltcsuite/lnd/tlv => ../lnd/tlv - -replace github.com/ltcsuite/ltcd/ltcutil/psbt => ../ltcd/ltcutil/psbt - -replace github.com/ltcsuite/ltcwallet/wallet/txauthor => ./wallet/txauthor - -replace github.com/ltcsuite/ltcwallet/wallet/txsizes => ./wallet/txsizes - -// loshy temp: - -replace github.com/ltcsuite/ltcd => ../ltcd - -replace github.com/ltcsuite/ltcd/chaincfg/chainhash => ../ltcd/chaincfg/chainhash diff --git a/go.sum b/go.sum index 43fcbe4d67..e2ffe87399 100644 --- a/go.sum +++ b/go.sum @@ -1,61 +1,21 @@ -github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= -github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/decred/dcrd/lru v1.1.1 h1:kWFDaW0OWx6AD6Ki342c+JPmHbiVdE6rK81pT3fuo/Y= -github.com/decred/dcrd/lru v1.1.1/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= -github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= -github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -68,210 +28,40 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= -github.com/ltcsuite/lnd/clock v0.0.0-20200822020009-1a001cbb895a/go.mod h1:d474AXivZyx25TMDB2tjjiQNuPrybFcgz+yl7vQgFTs= -github.com/ltcsuite/lnd/clock v1.1.0 h1:am2L2QTKgotbUSILSLLdZNe1ziSacXfR34Ry0f8npCs= -github.com/ltcsuite/lnd/clock v1.1.0/go.mod h1:Nn3X2+B0/70MnytJPHvOf3SPTPno09v90J/KJf5/dA8= -github.com/ltcsuite/lnd/queue v1.1.0 h1:/aVgox4Lz74xBU8BSw5HDau7hHl2irJs5M9u1SPQ2E0= -github.com/ltcsuite/lnd/queue v1.1.0/go.mod h1:DJrxK2gPC2FjJAVYxPOcnY2CplI3rhL2PEq7IexlTWQ= -github.com/ltcsuite/lnd/ticker v1.0.1/go.mod h1:WZKpekfDVAVv7Gsrr0GAWC/U1XURfGesFg9sQYJbeL4= -github.com/ltcsuite/lnd/ticker v1.1.0 h1:3zYM/JlKNqq+DotW8jiEdHVeY5Yl7n7cUb2bTof1yXM= -github.com/ltcsuite/lnd/ticker v1.1.0/go.mod h1:K2qQ3EPe8enztYvn/VAbLafCPy13XHMuCRPsySoAbt8= -github.com/ltcsuite/ltcd/btcec/v2 v2.1.0/go.mod h1:Vc9ZYXMcl5D6bA0VwMvGRDJYggO3YZ7/BuIri02Lq0E= -github.com/ltcsuite/ltcd/btcec/v2 v2.3.2 h1:HVArUNQGqGaSSoyYkk9qGht74U0/uNhS0n7jV9rkmno= -github.com/ltcsuite/ltcd/btcec/v2 v2.3.2/go.mod h1:T1t5TjbjPnryvlGQ+RpSKGuU8KhjNN7rS5+IznPj1VM= -github.com/ltcsuite/ltcd/ltcutil v1.1.0/go.mod h1:VbZlcopVgQteiCC5KRjIuxXH5wi1CtzhsvoYZ3K7FaE= -github.com/ltcsuite/ltcd/ltcutil v1.1.3 h1:8AapjCPLIt/wtYe6Odfk1EC2y9mcbpgjyxyCoNjAkFI= -github.com/ltcsuite/ltcd/ltcutil v1.1.3/go.mod h1:z8txd/ohBFrOMBUT70K8iZvHJD/Vc3gzx+6BP6cBxQw= -github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 h1:P6H9zsMpBBuGOsp9lnil7XfPaPujDqrbcmkqvDdiSiI= -github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0/go.mod h1:lmA2Ozxvbr2M8Mqb6ugOv5/FQT6x2Qnwg3yT/NiWEks= -github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0 h1:5pM7L26/OzJMcwQqGwGKIZvPIBt1Er4Ve9Ymc9KK6gc= -github.com/ltcsuite/ltcwallet/wtxmgr v1.5.0/go.mod h1:jAnztxV6d2JykUlGLCO5yNvtosmFaGMshBx0kps+K+M= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= -github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= -github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= -github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= -github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= -github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= -github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= -github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= -github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= -github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= -go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index 60100ca66e..ed5f70429e 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -692,6 +692,8 @@ func getNewAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { keyScope = waddrmgr.KeyScopeBIP0049Plus case "bech32": keyScope = waddrmgr.KeyScopeBIP0084 + case "mweb": + keyScope = waddrmgr.KeyScopeMweb case "legacy": // default if unset default: return nil, &ErrAddressTypeUnknown @@ -1391,7 +1393,7 @@ func sendPairs(w *wallet.Wallet, amounts map[string]ltcutil.Amount, return "", err } tx, err := w.SendOutputs( - outputs, &keyScope, account, minconf, feeSatPerKb, + outputs, nil, account, minconf, feeSatPerKb, wallet.CoinSelectionLargest, "", ) if err != nil { diff --git a/waddrmgr/address.go b/waddrmgr/address.go index 5a021d8ea6..a891694c48 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -15,6 +15,7 @@ import ( "github.com/ltcsuite/ltcd/btcec/v2/schnorr" "github.com/ltcsuite/ltcd/ltcutil" "github.com/ltcsuite/ltcd/ltcutil/hdkeychain" + "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" "github.com/ltcsuite/ltcd/txscript" "github.com/ltcsuite/ltcwallet/internal/zero" "github.com/ltcsuite/ltcwallet/walletdb" @@ -80,6 +81,9 @@ const ( // TaprootScript represents a p2tr (pay-to-taproot) address type that // commits to a script and not just a single key. TaprootScript + + // Mweb represents an MWEB address. + Mweb ) const ( @@ -170,7 +174,7 @@ type ValidatableManagedAddress interface { // // 3. We're able to generate a valid ECDSA/Schnorr signature based on // the passed private key validated against the internal public key. - Validate(msg [32]byte, priv *btcec.PrivateKey) error + Validate(msg [32]byte, priv, scan *btcec.PrivateKey) error } // ManagedScriptAddress extends ManagedAddress and represents a pay-to-script-hash @@ -294,6 +298,8 @@ func (a *managedAddress) AddrHash() []byte { hash = n.Hash160()[:] case *ltcutil.AddressTaproot: hash = n.WitnessProgram() + case *ltcutil.AddressMweb: + hash = ltcutil.Hash160(n.ScriptAddress()) } return hash @@ -326,7 +332,7 @@ func (a *managedAddress) Compressed() bool { // // This is part of the ManagedAddress interface implementation. func (a *managedAddress) Used(ns walletdb.ReadBucket) bool { - return a.manager.fetchUsed(ns, a.AddrHash()) + return a.manager.fetchUsed(ns, a.address.ScriptAddress()) } // PubKey returns the public key associated with the address. @@ -361,19 +367,22 @@ func (a *managedAddress) ExportPubKey() string { // // This is part of the ManagedPubKeyAddress interface implementation. func (a *managedAddress) PrivKey() (*btcec.PrivateKey, error) { + a.manager.rootManager.mtx.RLock() + defer a.manager.rootManager.mtx.RUnlock() + // No private keys are available for a watching-only address manager. - if a.manager.rootManager.WatchOnly() { + if a.manager.rootManager.watchOnly() { return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) } - a.manager.mtx.Lock() - defer a.manager.mtx.Unlock() - // Account manager must be unlocked to decrypt the private key. - if a.manager.rootManager.IsLocked() { + if a.manager.rootManager.isLocked() { return nil, managerError(ErrLocked, errLocked, nil) } + a.manager.mtx.Lock() + defer a.manager.mtx.Unlock() + // Decrypt the key as needed. Also, make sure it's a copy since the // private key stored in memory can be cleared at any time. Otherwise // the returned private key could be invalidated from under the caller. @@ -439,7 +448,9 @@ type signature interface { // the passed private key validated against the internal public key. // // NOTE: This is part of the ValidatableManagedAddress interface . -func (a *managedAddress) Validate(msg [32]byte, priv *btcec.PrivateKey) error { +func (a *managedAddress) Validate(msg [32]byte, + priv, scan *btcec.PrivateKey) error { + // First, we'll obtain the mapping public key from the target private // key. This key should match up with the public key we store // internally. @@ -460,7 +471,7 @@ func (a *managedAddress) Validate(msg [32]byte, priv *btcec.PrivateKey) error { // This can potentially catch a hardware/software error when mapping // the public key to a Bitcoin address. addr, err := newManagedAddressWithoutPrivKey( - a.manager, a.derivationPath, a.pubKey, a.compressed, a.addrType, + a.manager, a.derivationPath, a.pubKey, scan, a.compressed, a.addrType, ) if err != nil { return fmt.Errorf("unable to re-create addr: %w", err) @@ -483,7 +494,7 @@ func (a *managedAddress) Validate(msg [32]byte, priv *btcec.PrivateKey) error { switch a.addrType { // For the "legacy" addr types, we'll generate an ECDSA signature to // verify against. - case NestedWitnessPubKey, PubKeyHash, WitnessPubKey: + case NestedWitnessPubKey, PubKeyHash, WitnessPubKey, Mweb: sig = ecdsa.Sign(addrPrivKey, msg[:]) // For the newer taproot addr type, we'll generate a schnorr signature @@ -511,7 +522,8 @@ func (a *managedAddress) Validate(msg [32]byte, priv *btcec.PrivateKey) error { // passed account, public key, and whether or not the public key should be // compressed. func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, - derivationPath DerivationPath, pubKey *btcec.PublicKey, compressed bool, + derivationPath DerivationPath, pubKey *btcec.PublicKey, + scanKey *btcec.PrivateKey, compressed bool, addrType AddressType) (*managedAddress, error) { // Create a pay-to-pubkey-hash address from the public key. @@ -583,6 +595,18 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, if err != nil { return nil, err } + + case Mweb: + scanSecret := scanKey.Serialize() + defer zero.Bytes(scanSecret) + spendPubKey := (*mw.PublicKey)(pubKey.SerializeCompressed()) + address = ltcutil.NewAddressMweb( + &mw.StealthAddress{ + Scan: spendPubKey.Mul((*mw.SecretKey)(scanSecret)), + Spend: spendPubKey, + }, + m.rootManager.chainParams, + ) } return &managedAddress{ @@ -603,7 +627,7 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, // private key, and whether or not the public key is compressed. The managed // address will have access to the private and public keys. func newManagedAddress(s *ScopedKeyManager, derivationPath DerivationPath, - privKey *btcec.PrivateKey, compressed bool, + privKey, scanKey *btcec.PrivateKey, compressed bool, addrType AddressType, acctInfo *accountInfo) (*managedAddress, error) { // Encrypt the private key. @@ -621,7 +645,7 @@ func newManagedAddress(s *ScopedKeyManager, derivationPath DerivationPath, // and then add the private key to it. ecPubKey := privKey.PubKey() managedAddr, err := newManagedAddressWithoutPrivKey( - s, derivationPath, ecPubKey, compressed, addrType, + s, derivationPath, ecPubKey, scanKey, compressed, addrType, ) if err != nil { return nil, err @@ -641,7 +665,7 @@ func newManagedAddress(s *ScopedKeyManager, derivationPath DerivationPath, // We'll first validate things against the private key we got // from the original derivation. - err = managedAddr.Validate(msg, privKey) + err = managedAddr.Validate(msg, privKey, scanKey) if err != nil { return nil, fmt.Errorf("addr validation for addr=%v "+ "failed: %w", managedAddr.address, err) @@ -668,7 +692,7 @@ func newManagedAddress(s *ScopedKeyManager, derivationPath DerivationPath, if err != nil { return nil, fmt.Errorf("unable to gen priv key: %w", err) } - err = managedAddr.Validate(msg, freshPrivKey) + err = managedAddr.Validate(msg, freshPrivKey, scanKey) if err != nil { return nil, fmt.Errorf("addr validation for addr=%v "+ "failed after rederiving: %w", @@ -684,11 +708,19 @@ func newManagedAddress(s *ScopedKeyManager, derivationPath DerivationPath, // will only have access to the public key. func newManagedAddressFromExtKey(s *ScopedKeyManager, derivationPath DerivationPath, key *hdkeychain.ExtendedKey, - addrType AddressType, acctInfo *accountInfo) (*managedAddress, error) { + addrType AddressType, acctInfo *accountInfo) ( + managedAddr *managedAddress, err error) { + + var scanPrivKey *btcec.PrivateKey + if acctInfo.scanKey != nil { + scanPrivKey, err = acctInfo.scanKey.ECPrivKey() + if err != nil { + return nil, err + } + } // Create a new managed address based on the public or private key // depending on whether the generated key is private. - var managedAddr *managedAddress if key.IsPrivate() { privKey, err := key.ECPrivKey() if err != nil { @@ -698,7 +730,7 @@ func newManagedAddressFromExtKey(s *ScopedKeyManager, // Ensure the temp private key big integer is cleared after // use. managedAddr, err = newManagedAddress( - s, derivationPath, privKey, true, addrType, acctInfo, + s, derivationPath, privKey, scanPrivKey, true, addrType, acctInfo, ) if err != nil { return nil, err @@ -710,7 +742,7 @@ func newManagedAddressFromExtKey(s *ScopedKeyManager, } managedAddr, err = newManagedAddressWithoutPrivKey( - s, derivationPath, pubKey, true, + s, derivationPath, pubKey, scanPrivKey, true, addrType, ) if err != nil { @@ -854,19 +886,22 @@ func (a *scriptAddress) Used(ns walletdb.ReadBucket) bool { // // This is part of the ManagedAddress interface implementation. func (a *scriptAddress) Script() ([]byte, error) { + a.manager.rootManager.mtx.RLock() + defer a.manager.rootManager.mtx.RUnlock() + // No script is available for a watching-only address manager. - if a.manager.rootManager.WatchOnly() { + if a.manager.rootManager.watchOnly() { return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) } - a.manager.mtx.Lock() - defer a.manager.mtx.Unlock() - // Account manager must be unlocked to decrypt the script. - if a.manager.rootManager.IsLocked() { + if a.manager.rootManager.isLocked() { return nil, managerError(ErrLocked, errLocked, nil) } + a.manager.mtx.Lock() + defer a.manager.mtx.Unlock() + // Decrypt the script as needed. Also, make sure it's a copy since the // script stored in memory can be cleared at any time. Otherwise, // the returned script could be invalidated from under the caller. @@ -952,19 +987,22 @@ func (a *witnessScriptAddress) Used(ns walletdb.ReadBucket) bool { // // This is part of the ManagedAddress interface implementation. func (a *witnessScriptAddress) Script() ([]byte, error) { + a.manager.rootManager.mtx.RLock() + defer a.manager.rootManager.mtx.RUnlock() + // No script is available for a watching-only address manager. - if a.isSecretScript && a.manager.rootManager.WatchOnly() { + if a.isSecretScript && a.manager.rootManager.watchOnly() { return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) } - a.manager.mtx.Lock() - defer a.manager.mtx.Unlock() - // Account manager must be unlocked to decrypt the script. - if a.isSecretScript && a.manager.rootManager.IsLocked() { + if a.isSecretScript && a.manager.rootManager.isLocked() { return nil, managerError(ErrLocked, errLocked, nil) } + a.manager.mtx.Lock() + defer a.manager.mtx.Unlock() + cryptoKey := a.manager.rootManager.cryptoKeyScript if !a.isSecretScript { cryptoKey = a.manager.rootManager.cryptoKeyPub diff --git a/waddrmgr/common_test.go b/waddrmgr/common_test.go index 7e23140efa..8014dcb57a 100644 --- a/waddrmgr/common_test.go +++ b/waddrmgr/common_test.go @@ -213,6 +213,91 @@ var ( // generated from the seed expectedInternalAddrs = expectedAddrs[5:] + // expectedMwebAddrs is the list of expected MWEB addresses + // generated from the seed + expectedMwebAddrs = []expectedAddr{ + { + address: "ltcmweb1qqwq0kwggn9mjh48s4vlql66vhq34aq9er70dalswwp6p8nyfqftegq3mpf63hleancqyelvsguefcszq5ec66yjdsa9g05kum8xa34d77q9qdm8w", + addressHash: hexToBytes("56f589310b93de56fb2941729c567cd1c47f63e9"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("023b0a751bff3d9e004cfd9047329c4040a671ad124d874a87d2dcd9cdd8d5bef0"), + privKey: hexToBytes("097ff3dd1b2ef461cafc2e7ee4ec7bc7ff220a1941af4080398873a38ce2fcc7"), + privKeyWIF: "T3NSgNPRQuBrzwP1UqvybsCxmpHxgGSwveZtTQW3hWGdcoyeA6T9", + derivationInfo: DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 0, + }, + }, + { + address: "ltcmweb1qqvyy5rms0faeq0v9vagypgn6r9yn03usxkmxp73mtcam53n9d386yqm8tvl4y8lm72d5s02pr0nuheq4zzq303pllpc2el9cxq2wk0eahu2zscaf", + addressHash: hexToBytes("2a99811dc77aac2c920156eda8e0d1884742e193"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("03675b3f521ffbf29b483d411be7cbe415108117c43ff870acfcb83014eb3f3dbf"), + privKey: hexToBytes("eab71dee1b305a362fe98ae9fd182bbcdfefaf5acea9dc24f9a9988145edbf10"), + privKeyWIF: "TAvEUAGyjHw9cmtvmDecnZiX2KBgHd2JkQKyVaPEwRjekZJih1FN", + derivationInfo: DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 1, + }, + }, + { + address: "ltcmweb1qqty98evksf46twze0yhmdzxfxt2u5hsphjccjm33f4wksjvemwsfuqk8pdgcjz2aghpuhn947wskxhxcquhhx8y4f4sh4r365mlmpj6cq584w0xs", + addressHash: hexToBytes("c4ceac9fa0679db916251fb4e76a855b581f1891"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("02c70b5189095d45c3cbccb5f3a1635cd8072f731c954d617a8e3aa6ffb0cb5805"), + privKey: hexToBytes("d38ea91fbbd30fa137c1d8417830ccb56b111064bebd6c1d937332c80183b7bb"), + privKeyWIF: "TA9DXhfwwMTpLoKo1sPDVPthoJgvwRNTdVTATZmpU6TRJELuu6GS", + derivationInfo: DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 2, + }, + }, + { + address: "ltcmweb1qqv3k7h8rr430g9dfv4k0zwm24fs7pq2we8spdqcre6c9rn3wh7jdxqnk5jks4zvuyrx5acvwzxya92qhfwq9wa6wq60wrj2mslyqfm2f8s3snmjn", + addressHash: hexToBytes("589535d6187b55d4e8463f00081c788332bf0d57"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("0276a4ad0a899c20cd4ee18e1189d2a8174b8057774e069ee1c95b87c804ed493c"), + privKey: hexToBytes("bede8d40316af67b8fedc84e80ad17b265fb10b72db5251bd566df4fcea288e2"), + privKeyWIF: "T9T15i4pFiegQpod9AhNFMtkpj3MkPcVPypr3QLmBb8ocuXjPKse", + derivationInfo: DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 3, + }, + }, + { + address: "ltcmweb1qqtqqu3xrzv6v6zyjhvcudvpwf2mgu2hpqc5hndw68mjmmzurrnx36q348f3epm44rssxhrg23gteldsn9wlqjs6txmhlc2sa8za6uc0e6gnz5egd", + addressHash: hexToBytes("49a79439a3ba6cae642c36325370e032279fd385"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("02353a6390eeb51c206b8d0a8a179fb6132bbe09434b36effc2a1d38bbae61f9d2"), + privKey: hexToBytes("95edf80a505e033c512a1e07f8cd77e84e9e2ce53309257b328de2c321ff0a2c"), + privKeyWIF: "T85RMHZKnqyDYm7E24qwUHGvKpSPP3DH9UqXP3GzcH2UZTuBYdKk", + derivationInfo: DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 4, + }, + }, + } + // defaultDBTimeout specifies the timeout value when opening the wallet // database. defaultDBTimeout = 10 * time.Second diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 22a6e1d848..d18503f77d 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -104,11 +104,13 @@ type dbAccountRow struct { // BIP0044-like account in the database. type dbDefaultAccountRow struct { dbAccountRow - pubKeyEncrypted []byte - privKeyEncrypted []byte - nextExternalIndex uint32 - nextInternalIndex uint32 - name string + pubKeyEncrypted []byte + privKeyEncrypted []byte + scanKeyEncrypted []byte + spendPubKeyEncrypted []byte + nextExternalIndex uint32 + nextInternalIndex uint32 + name string } // dbWatchOnlyAccountRow houses additional information stored about a watch-only @@ -746,16 +748,20 @@ func serializeAccountRow(row *dbAccountRow) []byte { // account row as a BIP0044-like account. func deserializeDefaultAccountRow(accountID []byte, row *dbAccountRow) (*dbDefaultAccountRow, error) { // The serialized BIP0044 account raw data format is: - // - // + // + // + // // - // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes encrypted - // privkey len + encrypted privkey + 4 bytes next external index + + // 4 bytes encrypted pubkey len + encrypted pubkey + + // 4 bytes encrypted privkey len + encrypted privkey + + // 4 bytes encrypted scankey len + encrypted scankey + + // 4 bytes encrypted spendpubkey len + encrypted spendpubkey + + // 4 bytes next external index + // 4 bytes next internal index + 4 bytes name len + name // Given the above, the length of the entry must be at a minimum // the constant value sizes. - if len(row.rawData) < 20 { + if len(row.rawData) < 28 { str := fmt.Sprintf("malformed serialized bip0044 account for "+ "key %x", accountID) return nil, managerError(ErrDatabase, str, nil) @@ -774,6 +780,16 @@ func deserializeDefaultAccountRow(accountID []byte, row *dbAccountRow) (*dbDefau retRow.privKeyEncrypted = make([]byte, privLen) copy(retRow.privKeyEncrypted, row.rawData[offset:offset+privLen]) offset += privLen + scanLen := binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) + offset += 4 + retRow.scanKeyEncrypted = make([]byte, scanLen) + copy(retRow.scanKeyEncrypted, row.rawData[offset:offset+scanLen]) + offset += scanLen + spendLen := binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) + offset += 4 + retRow.spendPubKeyEncrypted = make([]byte, spendLen) + copy(retRow.spendPubKeyEncrypted, row.rawData[offset:offset+spendLen]) + offset += spendLen retRow.nextExternalIndex = binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) offset += 4 retRow.nextInternalIndex = binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) @@ -787,20 +803,27 @@ func deserializeDefaultAccountRow(accountID []byte, row *dbAccountRow) (*dbDefau // serializeDefaultAccountRow returns the serialization of the raw data field // for a BIP0044-like account. -func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey []byte, +func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey, + encryptedScanKey, encryptedSpendPubKey []byte, nextExternalIndex, nextInternalIndex uint32, name string) []byte { // The serialized BIP0044 account raw data format is: - // - // + // + // + // // - // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes encrypted - // privkey len + encrypted privkey + 4 bytes next external index + + // 4 bytes encrypted pubkey len + encrypted pubkey + + // 4 bytes encrypted privkey len + encrypted privkey + + // 4 bytes encrypted scankey len + encrypted scankey + + // 4 bytes encrypted spendpubkey len + encrypted spendpubkey + + // 4 bytes next external index + // 4 bytes next internal index + 4 bytes name len + name pubLen := uint32(len(encryptedPubKey)) privLen := uint32(len(encryptedPrivKey)) + scanLen := uint32(len(encryptedScanKey)) + spendLen := uint32(len(encryptedSpendPubKey)) nameLen := uint32(len(name)) - rawData := make([]byte, 20+pubLen+privLen+nameLen) + rawData := make([]byte, 28+pubLen+privLen+scanLen+spendLen+nameLen) binary.LittleEndian.PutUint32(rawData[0:4], pubLen) copy(rawData[4:4+pubLen], encryptedPubKey) offset := 4 + pubLen @@ -808,6 +831,14 @@ func serializeDefaultAccountRow(encryptedPubKey, encryptedPrivKey []byte, offset += 4 copy(rawData[offset:offset+privLen], encryptedPrivKey) offset += privLen + binary.LittleEndian.PutUint32(rawData[offset:offset+4], scanLen) + offset += 4 + copy(rawData[offset:offset+scanLen], encryptedScanKey) + offset += scanLen + binary.LittleEndian.PutUint32(rawData[offset:offset+4], spendLen) + offset += 4 + copy(rawData[offset:offset+spendLen], encryptedSpendPubKey) + offset += spendLen binary.LittleEndian.PutUint32(rawData[offset:offset+4], nextExternalIndex) offset += 4 binary.LittleEndian.PutUint32(rawData[offset:offset+4], nextInternalIndex) @@ -1254,12 +1285,14 @@ func putAccountRow(ns walletdb.ReadWriteBucket, scope *KeyScope, // putDefaultAccountInfo stores the provided default account information to the // database. func putDefaultAccountInfo(ns walletdb.ReadWriteBucket, scope *KeyScope, - account uint32, encryptedPubKey, encryptedPrivKey []byte, + account uint32, encryptedPubKey, encryptedPrivKey, + encryptedScanKey, encryptedSpendPubKey []byte, nextExternalIndex, nextInternalIndex uint32, name string) error { rawData := serializeDefaultAccountRow( - encryptedPubKey, encryptedPrivKey, nextExternalIndex, - nextInternalIndex, name, + encryptedPubKey, encryptedPrivKey, + encryptedScanKey, encryptedSpendPubKey, + nextExternalIndex, nextInternalIndex, name, ) // TODO(roasbeef): pass scope bucket directly?? @@ -1767,6 +1800,7 @@ func putChainedAddress(ns walletdb.ReadWriteBucket, scope *KeyScope, // Reserialize the account with the updated index and store it. row.rawData = serializeDefaultAccountRow( arow.pubKeyEncrypted, arow.privKeyEncrypted, + arow.scanKeyEncrypted, arow.spendPubKeyEncrypted, nextExternalIndex, nextInternalIndex, arow.name, ) @@ -2065,6 +2099,7 @@ func deletePrivateKeys(ns walletdb.ReadWriteBucket) error { // store it. row.rawData = serializeDefaultAccountRow( arow.pubKeyEncrypted, nil, + arow.scanKeyEncrypted, arow.spendPubKeyEncrypted, arow.nextExternalIndex, arow.nextInternalIndex, arow.name, ) @@ -2398,9 +2433,10 @@ func putBirthday(ns walletdb.ReadWriteBucket, t time.Time) error { // FetchBirthdayBlock retrieves the birthday block from the database. // // The block is serialized as follows: -// [0:4] block height -// [4:36] block hash -// [36:44] block timestamp +// +// [0:4] block height +// [4:36] block hash +// [36:44] block timestamp func FetchBirthdayBlock(ns walletdb.ReadBucket) (BlockStamp, error) { var block BlockStamp @@ -2438,9 +2474,10 @@ func DeleteBirthdayBlock(ns walletdb.ReadWriteBucket) error { // PutBirthdayBlock stores the provided birthday block to the database. // // The block is serialized as follows: -// [0:4] block height -// [4:36] block hash -// [36:44] block timestamp +// +// [0:4] block height +// [4:36] block hash +// [36:44] block timestamp // // NOTE: This does not alter the birthday block verification state. func PutBirthdayBlock(ns walletdb.ReadWriteBucket, block BlockStamp) error { diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index 857718fd93..03140700e4 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -154,6 +154,10 @@ type accountInfo struct { acctKeyPriv *hdkeychain.ExtendedKey acctKeyPub *hdkeychain.ExtendedKey + // Used to derive MWEB addresses and check for outputs. + scanKey *hdkeychain.ExtendedKey + spendPubKey *hdkeychain.ExtendedKey + // The external branch is used for all addresses which are intended for // external use. nextExternalIndex uint32 @@ -204,6 +208,14 @@ type AccountProperties struct { // NOTE: This may be nil for imported accounts. AccountPubKey *hdkeychain.ExtendedKey + // AccountScanKey is the account's scan private key that can be + // used to check for any MWEB outputs relevant to said account. + AccountScanKey *hdkeychain.ExtendedKey + + // AccountSpendPubKey is the account's spend public key that can + // be used to generate MWEB addresses for said account. + AccountSpendPubKey *hdkeychain.ExtendedKey + // MasterKeyFingerprint represents the fingerprint of the root key // corresponding to the master public key (also known as the key with // derivation path m/). This may be required by some hardware wallets @@ -593,7 +605,7 @@ func (m *Manager) NewScopedKeyManager(ns walletdb.ReadWriteBucket, // cointype key using the master HD private key, then encrypt // it along with the first account using our crypto keys. err = createManagerKeyScope( - ns, scope, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv, + ns, scope, scope.Coin, rootPriv, m.cryptoKeyPub, m.cryptoKeyPriv, ) if err != nil { return nil, err @@ -709,7 +721,7 @@ func (m *Manager) Address(ns walletdb.ReadBucket, // We'll iterate through each of the known scoped managers, and see if // any of them now of the target address. for _, scopedMgr := range m.scopedManagers { - addr, err := scopedMgr.Address(ns, address) + addr, err := scopedMgr.address(ns, address) if err != nil { continue } @@ -733,7 +745,7 @@ func (m *Manager) MarkUsed(ns walletdb.ReadWriteBucket, address ltcutil.Address) // First, we'll figure out which scoped manager this address belong to. for _, scopedMgr := range m.scopedManagers { - if _, err := scopedMgr.Address(ns, address); err != nil { + if _, err := scopedMgr.address(ns, address); err != nil { continue } @@ -757,7 +769,7 @@ func (m *Manager) AddrAccount(ns walletdb.ReadBucket, defer m.mtx.RUnlock() for _, scopedMgr := range m.scopedManagers { - if _, err := scopedMgr.Address(ns, address); err != nil { + if _, err := scopedMgr.address(ns, address); err != nil { continue } @@ -790,7 +802,7 @@ func (m *Manager) ForEachActiveAccountAddress(ns walletdb.ReadBucket, defer m.mtx.RUnlock() for _, scopedMgr := range m.scopedManagers { - err := scopedMgr.ForEachActiveAccountAddress(ns, account, fn) + err := scopedMgr.forEachActiveAccountAddress(ns, account, fn) if err != nil { return err } @@ -806,7 +818,7 @@ func (m *Manager) ForEachActiveAddress(ns walletdb.ReadBucket, fn func(addr ltcu defer m.mtx.RUnlock() for _, scopedMgr := range m.scopedManagers { - err := scopedMgr.ForEachActiveAddress(ns, fn) + err := scopedMgr.forEachActiveAddress(ns, fn) if err != nil { return err } @@ -840,9 +852,9 @@ func (m *Manager) ForEachRelevantActiveAddress(ns walletdb.ReadBucket, var err error if isDefaultKeyScope { - err = scopedMgr.ForEachActiveAddress(ns, fn) + err = scopedMgr.forEachActiveAddress(ns, fn) } else { - err = scopedMgr.ForEachInternalActiveAddress(ns, fn) + err = scopedMgr.forEachInternalActiveAddress(ns, fn) } if err != nil { return err @@ -861,7 +873,7 @@ func (m *Manager) ForEachAccountAddress(ns walletdb.ReadBucket, account uint32, defer m.mtx.RUnlock() for _, scopedMgr := range m.scopedManagers { - err := scopedMgr.ForEachAccountAddress(ns, account, fn) + err := scopedMgr.forEachAccountAddress(ns, account, fn) if err != nil { return err } @@ -1438,10 +1450,10 @@ func newManager(chainParams *chaincfg.Params, masterKeyPub *snacl.SecretKey, // In particular this is the hierarchical deterministic extended key path: // m/purpose'/' func deriveCoinTypeKey(masterNode *hdkeychain.ExtendedKey, - scope KeyScope) (*hdkeychain.ExtendedKey, error) { + scope KeyScope, coinType uint32) (*hdkeychain.ExtendedKey, error) { // Enforce maximum coin type. - if scope.Coin > maxCoinType { + if coinType > maxCoinType { err := managerError(ErrCoinTypeTooHigh, errCoinTypeTooHigh, nil) return nil, err } @@ -1467,7 +1479,7 @@ func deriveCoinTypeKey(masterNode *hdkeychain.ExtendedKey, // Derive the coin type key as a child of the purpose key. coinTypeKey, err := purpose.DeriveNonStandard( // nolint:staticcheck - scope.Coin + hdkeychain.HardenedKeyStart, + coinType + hdkeychain.HardenedKeyStart, ) if err != nil { return nil, err @@ -1686,16 +1698,19 @@ func Open(ns walletdb.ReadBucket, pubPassphrase []byte, // This partitions key derivation for a particular purpose+coin tuple, allowing // multiple address derivation schems to be maintained concurrently. func createManagerKeyScope(ns walletdb.ReadWriteBucket, - scope KeyScope, root *hdkeychain.ExtendedKey, + scope KeyScope, coinType uint32, root *hdkeychain.ExtendedKey, cryptoKeyPub, cryptoKeyPriv EncryptorDecryptor) error { // Derive the cointype key according to the passed scope. - coinTypeKeyPriv, err := deriveCoinTypeKey(root, scope) + coinTypeKeyPriv, err := deriveCoinTypeKey(root, scope, coinType) if err != nil { str := "failed to derive cointype extended key" return managerError(ErrKeyChain, str, err) } defer coinTypeKeyPriv.Zero() + if scope == KeyScopeLiteWallet { + coinTypeKeyPriv = root + } // Derive the account key for the first account according our // BIP0044-like derivation. @@ -1733,6 +1748,21 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, return managerError(ErrKeyChain, str, err) } + acctKeyScan, err := acctKeyPriv.Derive(hdkeychain.HardenedKeyStart) + if err != nil { + str := "failed to derive scan key" + return managerError(ErrKeyChain, str, err) + } + defer acctKeyScan.Zero() // Ensure key is zeroed when done. + + spendKey, err := acctKeyPriv.Derive(hdkeychain.HardenedKeyStart + 1) + if err != nil { + str := "failed to derive spend key" + return managerError(ErrKeyChain, str, err) + } + defer spendKey.Zero() // Ensure key is zeroed when done. + acctKeySpend, _ := spendKey.Neuter() + // Encrypt the cointype keys with the associated crypto keys. coinTypeKeyPub, err := coinTypeKeyPriv.Neuter() if err != nil { @@ -1753,7 +1783,7 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, // Encrypt the default account keys with the associated crypto keys. acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) if err != nil { - str := "failed to encrypt public key for account 0" + str := "failed to encrypt public key for account 0" return managerError(ErrCrypto, str, err) } acctPrivEnc, err := cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String())) @@ -1761,6 +1791,20 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, str := "failed to encrypt private key for account 0" return managerError(ErrCrypto, str, err) } + acctScanEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyScan.String())) + if err != nil { + str := "failed to encrypt scan key for account 0" + return managerError(ErrCrypto, str, err) + } + acctSpendEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeySpend.String())) + if err != nil { + str := "failed to encrypt spend key for account 0" + return managerError(ErrCrypto, str, err) + } + if scope != KeyScopeMweb { + acctScanEnc = nil + acctSpendEnc = nil + } // Save the encrypted cointype keys to the database. err = putCoinTypeKeys(ns, &scope, coinTypePubEnc, coinTypePrivEnc) @@ -1770,15 +1814,15 @@ func createManagerKeyScope(ns walletdb.ReadWriteBucket, // Save the information for the default account to the database. err = putDefaultAccountInfo( - ns, &scope, DefaultAccountNum, acctPubEnc, acctPrivEnc, 0, 0, - defaultAccountName, + ns, &scope, DefaultAccountNum, acctPubEnc, acctPrivEnc, + acctScanEnc, acctSpendEnc, 0, 0, defaultAccountName, ) if err != nil { return err } return putDefaultAccountInfo( - ns, &scope, ImportedAddrAccount, nil, nil, 0, 0, + ns, &scope, ImportedAddrAccount, nil, nil, nil, nil, 0, 0, ImportedAddrAccountName, ) } @@ -1942,7 +1986,7 @@ func Create(ns walletdb.ReadWriteBucket, rootKey *hdkeychain.ExtendedKey, // first default account. for _, defaultScope := range DefaultKeyScopes { err := createManagerKeyScope( - ns, defaultScope, rootKey, cryptoKeyPub, cryptoKeyPriv, + ns, defaultScope, chainParams.HDCoinType, rootKey, cryptoKeyPub, cryptoKeyPriv, ) if err != nil { return maybeConvertDbError(err) diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index ad778d7ace..47f95bc0c4 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -27,6 +27,10 @@ import ( "github.com/stretchr/testify/require" ) +func init() { + chaincfg.MainNetParams.HDCoinType = 0 +} + // failingCryptoKey is an implementation of the EncryptorDecryptor interface // with intentionally fails when attempting to encrypt or decrypt with it. type failingCryptoKey struct { @@ -347,12 +351,11 @@ func testAddress(tc *testContext, prefix string, gotAddr ManagedAddress, // generating multiple addresses via NextExternalAddresses, ensuring they can be // retrieved by Address, and that they work properly when the manager is locked // and unlocked. -func testExternalAddresses(tc *testContext) bool { +func testExternalAddresses(tc *testContext, expectedAddrs []expectedAddr) bool { prefix := testNamePrefix(tc) + " testExternalAddresses" var addrs []ManagedAddress if tc.create { prefix := prefix + " NextExternalAddresses" - var addrs []ManagedAddress err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) var err error @@ -365,10 +368,10 @@ func testExternalAddresses(tc *testContext) bool { tc.t.Errorf("%s: unexpected error: %v", prefix, err) return false } - if len(addrs) != len(expectedExternalAddrs) { + if len(addrs) != len(expectedAddrs) { tc.t.Errorf("%s: unexpected number of addresses - got "+ "%d, want %d", prefix, len(addrs), - len(expectedExternalAddrs)) + len(expectedAddrs)) return false } } @@ -382,7 +385,7 @@ func testExternalAddresses(tc *testContext) bool { // of the tests. for i := 0; i < len(addrs); i++ { prefix := fmt.Sprintf("%s ExternalAddress #%d", prefix, i) - if !testAddress(tc, prefix, addrs[i], &expectedExternalAddrs[i]) { + if !testAddress(tc, prefix, addrs[i], &expectedAddrs[i]) { return false } } @@ -402,20 +405,19 @@ func testExternalAddresses(tc *testContext) bool { tc.t.Errorf("%s: unexpected error: %v", leaPrefix, err) return false } - if !testAddress(tc, leaPrefix, lastAddr, &expectedExternalAddrs[len(expectedExternalAddrs)-1]) { + if !testAddress(tc, leaPrefix, lastAddr, &expectedAddrs[len(expectedAddrs)-1]) { return false } // Now, use the Address API to retrieve each of the expected new // addresses and ensure they're accurate. chainParams := tc.manager.ChainParams() - for i := 0; i < len(expectedExternalAddrs); i++ { - pkHash := expectedExternalAddrs[i].addressHash - utilAddr, err := ltcutil.NewAddressPubKeyHash( - pkHash, chainParams, + for i := 0; i < len(expectedAddrs); i++ { + utilAddr, err := ltcutil.DecodeAddress( + expectedAddrs[i].address, chainParams, ) if err != nil { - tc.t.Errorf("%s NewAddressPubKeyHash #%d: "+ + tc.t.Errorf("%s DecodeAddress #%d: "+ "unexpected error: %v", prefix, i, err) return false } @@ -434,7 +436,7 @@ func testExternalAddresses(tc *testContext) bool { return false } - if !testAddress(tc, prefix, addr, &expectedExternalAddrs[i]) { + if !testAddress(tc, prefix, addr, &expectedAddrs[i]) { return false } } @@ -1723,7 +1725,7 @@ func testForEachAccountAddress(tc *testContext) bool { prefix := fmt.Sprintf("%s: #%d", prefix, i) gotAddr := addrs[i] wantAddr := expectedAddrMap[gotAddr.Address().String()] - if !testAddress(tc, prefix, gotAddr, wantAddr) { + if wantAddr != nil && !testAddress(tc, prefix, gotAddr, wantAddr) { return false } delete(expectedAddrMap, gotAddr.Address().String()) @@ -1745,7 +1747,7 @@ func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) { if !caseCreatedWatchingOnly { // Test API for normal create (w/ seed) case. testLocking(tc) - testExternalAddresses(tc) + testExternalAddresses(tc, expectedExternalAddrs) testInternalAddresses(tc) testImportPrivateKey(tc) testImportScript(tc) @@ -1764,7 +1766,7 @@ func testManagerAPI(tc *testContext, caseCreatedWatchingOnly bool) { testRenameAccount(tc) } else { // Test API for created watch-only case. - testExternalAddresses(tc) + testExternalAddresses(tc, expectedExternalAddrs) testInternalAddresses(tc) testMarkUsed(tc, false) testChangePassphrase(tc) @@ -2073,7 +2075,7 @@ func testManagerCase(t *testing.T, caseName string, // Run all of the manager API tests in create mode and close the // manager after they've completed - testManagerAPI(&testContext{ + tc := &testContext{ t: t, caseName: caseName, db: db, @@ -2082,7 +2084,18 @@ func testManagerCase(t *testing.T, caseName string, internalAccount: 0, create: true, watchingOnly: caseCreatedWatchingOnly, - }, caseCreatedWatchingOnly) + } + testManagerAPI(tc, caseCreatedWatchingOnly) + + if !caseCreatedWatchingOnly { + // Test MWEB address derivation + tc.manager, err = mgr.FetchScopedKeyManager(KeyScopeMweb) + if err != nil { + t.Fatalf("(%s) unable to fetch default scope: %v", caseName, err) + } + tc.internalAccount = 0 + testExternalAddresses(tc, expectedMwebAddrs) + } mgr.Close() // Open the manager and run all the tests again in open mode which @@ -2103,7 +2116,7 @@ func testManagerCase(t *testing.T, caseName string, if err != nil { t.Fatalf("(%s) unable to fetch default scope: %v", caseName, err) } - tc := &testContext{ + tc = &testContext{ t: t, caseName: caseName, db: db, @@ -2143,7 +2156,8 @@ func deriveTestAccountKey(t *testing.T) *hdkeychain.ExtendedKey { t.Errorf("NewMaster: unexpected error: %v", err) return nil } - scopeKey, err := deriveCoinTypeKey(masterKey, KeyScopeBIP0044) + scopeKey, err := deriveCoinTypeKey(masterKey, KeyScopeBIP0044, + chaincfg.MainNetParams.HDCoinType) if err != nil { t.Errorf("derive: unexpected error: %v", err) return nil @@ -3245,6 +3259,7 @@ func TestManagedAddressValidation(t *testing.T) { ) var addr ManagedAddress + var scanKey *btcec.PrivateKey // With the scoped managed we created above, // generate a new address. @@ -3257,6 +3272,17 @@ func TestManagedAddressValidation(t *testing.T) { return err } + props, err := scopedMgr.AccountProperties(ns, 0) + if err != nil { + return err + } + if props.AccountScanKey != nil { + scanKey, err = props.AccountScanKey.ECPrivKey() + if err != nil { + return err + } + } + addr = addrs[0] return nil }) @@ -3275,7 +3301,7 @@ func TestManagedAddressValidation(t *testing.T) { var msg [32]byte require.ErrorIs( - t, pubKeyAddr.Validate(msg, privKeyForAddr), + t, pubKeyAddr.Validate(msg, privKeyForAddr, scanKey), testCase.expectedErr, ) }) diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 2747bb361b..199f387a7f 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -11,6 +11,8 @@ import ( "github.com/ltcsuite/ltcd/chaincfg" "github.com/ltcsuite/ltcd/ltcutil" "github.com/ltcsuite/ltcd/ltcutil/hdkeychain" + "github.com/ltcsuite/ltcd/ltcutil/mweb" + "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" "github.com/ltcsuite/ltcd/txscript" "github.com/ltcsuite/ltcd/wire" "github.com/ltcsuite/ltcwallet/internal/zero" @@ -196,7 +198,19 @@ var ( // it. KeyScopeBIP0044 = KeyScope{ Purpose: 44, - Coin: 0, + Coin: 2, + } + + // KeyScopeMweb is the key scope for MWEB derivation. + KeyScopeMweb = KeyScope{ + Purpose: 1000, + Coin: 2, + } + + // KeyScopeLiteWallet is the key scope for LiteWallet derivation. + KeyScopeLiteWallet = KeyScope{ + Purpose: 9999, + Coin: 2, } // DefaultKeyScopes is the set of default key scopes that will be @@ -206,6 +220,8 @@ var ( KeyScopeBIP0084, KeyScopeBIP0086, KeyScopeBIP0044, + KeyScopeMweb, + KeyScopeLiteWallet, } // ScopeAddrMap is a map from the default key scopes to the scope @@ -228,6 +244,14 @@ var ( InternalAddrType: PubKeyHash, ExternalAddrType: PubKeyHash, }, + KeyScopeMweb: { + InternalAddrType: Mweb, + ExternalAddrType: Mweb, + }, + KeyScopeLiteWallet: { + InternalAddrType: PubKeyHash, + ExternalAddrType: PubKeyHash, + }, } // KeyScopeBIP0049AddrSchema is the address schema for the traditional @@ -394,6 +418,10 @@ func (s *ScopedKeyManager) deriveKey(acctInfo *accountInfo, branch, acctKey = acctInfo.acctKeyPriv } + if acctInfo.scanKey != nil { + return s.deriveSpendKey(acctKey, acctInfo, index) + } + // Derive and return the key. branchKey, err := acctKey.DeriveNonStandard(branch) // nolint:staticcheck if err != nil { @@ -416,6 +444,39 @@ func (s *ScopedKeyManager) deriveKey(acctInfo *accountInfo, branch, return addressKey, nil } +// deriveSpendKey returns either a public or private derived MWEB spend key +// for the given extended key, account info, and index. +func (s *ScopedKeyManager) deriveSpendKey(key *hdkeychain.ExtendedKey, + acctInfo *accountInfo, index uint32) (*hdkeychain.ExtendedKey, error) { + + scanKeyPriv, _ := acctInfo.scanKey.ECPrivKey() + defer scanKeyPriv.Zero() + scanSecret := mw.SecretKey(scanKeyPriv.Key.Bytes()) + defer zero.Bytes(scanSecret[:]) + + var keyBytes []byte + if key.IsPrivate() { + spendKey, err := key.Derive(hdkeychain.HardenedKeyStart + 1) + if err != nil { + str := "failed to derive spend key" + return nil, managerError(ErrKeyChain, str, err) + } + defer spendKey.Zero() // Ensure key is zeroed when done. + spendKeyPriv, _ := spendKey.ECPrivKey() + defer spendKeyPriv.Zero() + spendSecret := mw.SecretKey(spendKeyPriv.Key.Bytes()) + defer zero.Bytes(spendSecret[:]) + keychain := &mweb.Keychain{Scan: &scanSecret, Spend: &spendSecret} + keyBytes = keychain.SpendKey(index)[:] + } else { + spendKeyPub, _ := acctInfo.spendPubKey.ECPubKey() + spendPubKey := (*mw.PublicKey)(spendKeyPub.SerializeCompressed()) + keychain := &mweb.Keychain{Scan: &scanSecret, SpendPubKey: spendPubKey} + keyBytes = keychain.Address(index).Spend[:] + } + return hdkeychain.NewExtendedKey(nil, keyBytes, nil, nil, 0, 0, key.IsPrivate()), nil +} + // loadAccountInfo attempts to load and cache information about the given // account from the database. This includes what is necessary to derive new // keys for it and track the state of the internal and external branches. @@ -488,6 +549,28 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, } } + // Use the crypto public key to decrypt the account + // scan key and spend pubkey. + if len(row.scanKeyEncrypted) > 0 { + acctInfo.scanKey, err = decryptKey( + s.rootManager.cryptoKeyPub, row.scanKeyEncrypted, + ) + if err != nil { + str := fmt.Sprintf("failed to decrypt scan key "+ + "for account %d", account) + return nil, managerError(ErrCrypto, str, err) + } + + acctInfo.spendPubKey, err = decryptKey( + s.rootManager.cryptoKeyPub, row.spendPubKeyEncrypted, + ) + if err != nil { + str := fmt.Sprintf("failed to decrypt spend key "+ + "for account %d", account) + return nil, managerError(ErrCrypto, str, err) + } + } + case *dbWatchOnlyAccountRow: acctInfo = &accountInfo{ acctName: row.name, @@ -570,6 +653,9 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket, func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket, account uint32) (*AccountProperties, error) { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -598,8 +684,10 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket, props.ExternalKeyCount = acctInfo.nextExternalIndex props.InternalKeyCount = acctInfo.nextInternalIndex props.AccountPubKey = acctInfo.acctKeyPub + props.AccountScanKey = acctInfo.scanKey + props.AccountSpendPubKey = acctInfo.spendPubKey props.MasterKeyFingerprint = acctInfo.masterKeyFingerprint - props.IsWatchOnly = s.rootManager.WatchOnly() || + props.IsWatchOnly = s.rootManager.watchOnly() || acctInfo.acctKeyPriv == nil props.AddrSchema = acctInfo.addrSchema @@ -621,7 +709,7 @@ func (s *ScopedKeyManager) AccountProperties(ns walletdb.ReadBucket, } } else { props.AccountName = ImportedAddrAccountName // reserved, nonchangable - props.IsWatchOnly = s.rootManager.WatchOnly() + props.IsWatchOnly = s.rootManager.watchOnly() // Could be more efficient if this was tracked by the db. var importedKeyCount uint32 @@ -664,6 +752,9 @@ func (c *cachedKey) Size() (uint64, error) { func (s *ScopedKeyManager) DeriveFromKeyPathCache( kp DerivationPath) (*btcec.PrivateKey, error) { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -687,8 +778,8 @@ func (s *ScopedKeyManager) DeriveFromKeyPathCache( ) } - watchOnly := s.rootManager.WatchOnly() - private := !s.rootManager.IsLocked() && !watchOnly + watchOnly := s.rootManager.watchOnly() + private := !s.rootManager.isLocked() && !watchOnly // Now that we have the account information, we can derive the key // directly. @@ -720,11 +811,14 @@ func (s *ScopedKeyManager) DeriveFromKeyPathCache( func (s *ScopedKeyManager) DeriveFromKeyPath(ns walletdb.ReadBucket, kp DerivationPath) (ManagedAddress, error) { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() - watchOnly := s.rootManager.WatchOnly() - private := !s.rootManager.IsLocked() && !watchOnly + watchOnly := s.rootManager.watchOnly() + private := !s.rootManager.isLocked() && !watchOnly addrKey, _, _, err := s.deriveKeyFromPath( ns, kp.InternalAccount, kp.Branch, kp.Index, private, @@ -822,7 +916,7 @@ func (s *ScopedKeyManager) importedAddressRowToManaged(row *dbImportedAddressRow // TODO: Handle imported key being part of internal branch. compressed := len(pubBytes) == btcec.PubKeyBytesLenCompressed ma, err := newManagedAddressWithoutPrivKey( - s, ImportedDerivationPath, pubKey, compressed, + s, ImportedDerivationPath, pubKey, nil, compressed, s.addrSchema.ExternalAddrType, ) if err != nil { @@ -948,6 +1042,15 @@ func (s *ScopedKeyManager) existsAddress(ns walletdb.ReadBucket, addressID []byt func (s *ScopedKeyManager) Address(ns walletdb.ReadBucket, address ltcutil.Address) (ManagedAddress, error) { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + + return s.address(ns, address) +} + +func (s *ScopedKeyManager) address(ns walletdb.ReadBucket, + address ltcutil.Address) (ManagedAddress, error) { + // ScriptAddress will only return a script hash if we're accessing an // address that is either PKH or SH. In the event we're passed a PK // address, convert the PK to PKH address so that we can access it from @@ -1020,8 +1123,8 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, // Choose the account key to used based on whether the address manager // is locked. acctKey := acctInfo.acctKeyPub - watchOnly := s.rootManager.WatchOnly() || len(acctInfo.acctKeyEncrypted) == 0 - if !s.rootManager.IsLocked() && !watchOnly { + watchOnly := s.rootManager.watchOnly() || len(acctInfo.acctKeyEncrypted) == 0 + if !s.rootManager.isLocked() && !watchOnly { acctKey = acctInfo.acctKeyPriv } @@ -1065,6 +1168,15 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, // invalid, so use a loop to derive the next valid child. var nextKey *hdkeychain.ExtendedKey for { + if acctInfo.scanKey != nil { + nextKey, err = s.deriveSpendKey(acctKey, acctInfo, nextIndex) + if err != nil { + return nil, err + } + nextIndex++ + break + } + // Derive the next child in the external chain branch. key, err := branchKey.DeriveNonStandard(nextIndex) // nolint:staticcheck if err != nil { @@ -1192,6 +1304,9 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, // gets committed, we won't longer be holding the manager's // mutex at that point. We must therefore re-acquire it before // continuing. + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1242,8 +1357,8 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket, // Choose the account key to used based on whether the address manager // is locked. acctKey := acctInfo.acctKeyPub - watchOnly := s.rootManager.WatchOnly() || acctInfo.acctKeyPriv != nil - if !s.rootManager.IsLocked() && !watchOnly { + watchOnly := s.rootManager.watchOnly() || acctInfo.acctKeyPriv != nil + if !s.rootManager.isLocked() && !watchOnly { acctKey = acctInfo.acctKeyPriv } @@ -1294,6 +1409,15 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket, // invalid, so use a loop to derive the next valid child. var nextKey *hdkeychain.ExtendedKey for { + if acctInfo.scanKey != nil { + nextKey, err = s.deriveSpendKey(acctKey, acctInfo, nextIndex) + if err != nil { + return err + } + nextIndex++ + break + } + // Derive the next child in the external chain branch. key, err := branchKey.DeriveNonStandard(nextIndex) // nolint:staticcheck if err != nil { @@ -1392,7 +1516,7 @@ func (s *ScopedKeyManager) extendAddresses(ns walletdb.ReadWriteBucket, // Add the new managed address to the list of addresses that // need their private keys derived when the address manager is // next unlocked. - if s.rootManager.IsLocked() && !watchOnly { + if s.rootManager.isLocked() && !watchOnly { s.deriveOnUnlock = append(s.deriveOnUnlock, info) } } @@ -1421,6 +1545,9 @@ func (s *ScopedKeyManager) NextExternalAddresses(ns walletdb.ReadWriteBucket, return nil, err } + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1438,6 +1565,9 @@ func (s *ScopedKeyManager) NextInternalAddresses(ns walletdb.ReadWriteBucket, return nil, err } + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1456,6 +1586,9 @@ func (s *ScopedKeyManager) ExtendExternalAddresses(ns walletdb.ReadWriteBucket, return err } + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1474,6 +1607,9 @@ func (s *ScopedKeyManager) ExtendInternalAddresses(ns walletdb.ReadWriteBucket, return err } + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1497,6 +1633,9 @@ func (s *ScopedKeyManager) LastExternalAddress(ns walletdb.ReadBucket, return nil, err } + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1531,6 +1670,9 @@ func (s *ScopedKeyManager) LastInternalAddress(ns walletdb.ReadBucket, return nil, err } + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1553,17 +1695,20 @@ func (s *ScopedKeyManager) LastInternalAddress(ns walletdb.ReadBucket, // number *directly*, rather than taking a string name for the account, then // mapping that to the next highest account number. func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uint32) error { - if s.rootManager.WatchOnly() { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + + if s.rootManager.watchOnly() { return managerError(ErrWatchingOnly, errWatchingOnly, nil) } - s.mtx.Lock() - defer s.mtx.Unlock() - - if s.rootManager.IsLocked() { + if s.rootManager.isLocked() { return managerError(ErrLocked, errLocked, nil) } + s.mtx.Lock() + defer s.mtx.Unlock() + // As this is an ad hoc account that may not follow our normal linear // derivation, we'll create a new name for this account based off of // the account number. @@ -1589,6 +1734,9 @@ func (s *ScopedKeyManager) NewRawAccountWatchingOnly( pubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32, addrSchema *ScopeAddrSchema) error { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1607,17 +1755,20 @@ func (s *ScopedKeyManager) NewRawAccountWatchingOnly( // access to the cointype keys (from which extended account keys are derived), // it requires the manager to be unlocked. func (s *ScopedKeyManager) NewAccount(ns walletdb.ReadWriteBucket, name string) (uint32, error) { - if s.rootManager.WatchOnly() { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + + if s.rootManager.watchOnly() { return 0, managerError(ErrWatchingOnly, errWatchingOnly, nil) } - s.mtx.Lock() - defer s.mtx.Unlock() - - if s.rootManager.IsLocked() { + if s.rootManager.isLocked() { return 0, managerError(ErrLocked, errLocked, nil) } + s.mtx.Lock() + defer s.mtx.Unlock() + // Fetch latest account, and create a new account in the same // transaction Fetch the latest account number to generate the next // account number @@ -1689,12 +1840,27 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket, return managerError(ErrKeyChain, str, err) } + acctKeyScan, err := acctKeyPriv.Derive(hdkeychain.HardenedKeyStart) + if err != nil { + str := "failed to derive scan key" + return managerError(ErrKeyChain, str, err) + } + defer acctKeyScan.Zero() // Ensure key is zeroed when done. + + spendKey, err := acctKeyPriv.Derive(hdkeychain.HardenedKeyStart + 1) + if err != nil { + str := "failed to derive spend key" + return managerError(ErrKeyChain, str, err) + } + defer spendKey.Zero() // Ensure key is zeroed when done. + acctKeySpend, _ := spendKey.Neuter() + // Encrypt the default account keys with the associated crypto keys. acctPubEnc, err := s.rootManager.cryptoKeyPub.Encrypt( []byte(acctKeyPub.String()), ) if err != nil { - str := "failed to encrypt public key for account" + str := "failed to encrypt public key for account" return managerError(ErrCrypto, str, err) } acctPrivEnc, err := s.rootManager.cryptoKeyPriv.Encrypt( @@ -1704,11 +1870,30 @@ func (s *ScopedKeyManager) newAccount(ns walletdb.ReadWriteBucket, str := "failed to encrypt private key for account" return managerError(ErrCrypto, str, err) } + acctScanEnc, err := s.rootManager.cryptoKeyPub.Encrypt( + []byte(acctKeyScan.String()), + ) + if err != nil { + str := "failed to encrypt scan key for account" + return managerError(ErrCrypto, str, err) + } + acctSpendEnc, err := s.rootManager.cryptoKeyPub.Encrypt( + []byte(acctKeySpend.String()), + ) + if err != nil { + str := "failed to encrypt spend key for account" + return managerError(ErrCrypto, str, err) + } + if s.scope != KeyScopeMweb { + acctScanEnc = nil + acctSpendEnc = nil + } // We have the encrypted account extended keys, so save them to the // database err = putDefaultAccountInfo( - ns, &s.scope, account, acctPubEnc, acctPrivEnc, 0, 0, name, + ns, &s.scope, account, acctPubEnc, acctPrivEnc, + acctScanEnc, acctSpendEnc, 0, 0, name, ) if err != nil { return err @@ -1732,6 +1917,9 @@ func (s *ScopedKeyManager) NewAccountWatchingOnly(ns walletdb.ReadWriteBucket, name string, pubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32, addrSchema *ScopeAddrSchema) (uint32, error) { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -1852,9 +2040,9 @@ func (s *ScopedKeyManager) RenameAccount(ns walletdb.ReadWriteBucket, } err = putDefaultAccountInfo( - ns, &s.scope, account, row.pubKeyEncrypted, - row.privKeyEncrypted, row.nextExternalIndex, - row.nextInternalIndex, name, + ns, &s.scope, account, row.pubKeyEncrypted, row.privKeyEncrypted, + row.scanKeyEncrypted, row.spendPubKeyEncrypted, + row.nextExternalIndex, row.nextInternalIndex, name, ) if err != nil { return err @@ -1920,17 +2108,17 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, return nil, managerError(ErrWrongNet, str, nil) } - s.mtx.Lock() - defer s.mtx.Unlock() + s.rootManager.mtx.Lock() + defer s.rootManager.mtx.Unlock() // The manager must be unlocked to encrypt the imported private key. - if s.rootManager.IsLocked() && !s.rootManager.WatchOnly() { + if s.rootManager.isLocked() && !s.rootManager.watchOnly() { return nil, managerError(ErrLocked, errLocked, nil) } // Encrypt the private key when not a watching-only address manager. var encryptedPrivKey []byte - if !s.rootManager.WatchOnly() { + if !s.rootManager.watchOnly() { privKeyBytes := wif.PrivKey.Serialize() var err error encryptedPrivKey, err = s.rootManager.cryptoKeyPriv.Encrypt(privKeyBytes) @@ -1942,6 +2130,9 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, } } + s.mtx.Lock() + defer s.mtx.Unlock() + err := s.importPublicKey( ns, wif.SerializePubKey(), encryptedPrivKey, s.addrSchema.ExternalAddrType, bs, @@ -1951,7 +2142,7 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, } // Create a new managed address based on the imported address. - if !s.rootManager.WatchOnly() { + if !s.rootManager.watchOnly() { return s.toImportedPrivateManagedAddress(wif) } pubKey := wif.PrivKey.PubKey() @@ -1965,6 +2156,9 @@ func (s *ScopedKeyManager) ImportPrivateKey(ns walletdb.ReadWriteBucket, func (s *ScopedKeyManager) ImportPublicKey(ns walletdb.ReadWriteBucket, pubKey *btcec.PublicKey, bs *BlockStamp) (ManagedAddress, error) { + s.rootManager.mtx.Lock() + defer s.rootManager.mtx.Unlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -2040,10 +2234,8 @@ func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket, // The start block needs to be updated when the newly imported address // is before the current one. - s.rootManager.mtx.Lock() updateStartBlock := bs != nil && bs.Height < s.rootManager.syncState.startBlock.Height - s.rootManager.mtx.Unlock() // Save the new imported address to the db and update start block (if // needed) in a single transaction. @@ -2065,9 +2257,7 @@ func (s *ScopedKeyManager) importPublicKey(ns walletdb.ReadWriteBucket, // Now that the database has been updated, update the start block in // memory too if needed. if updateStartBlock { - s.rootManager.mtx.Lock() s.rootManager.syncState.startBlock = *bs - s.rootManager.mtx.Unlock() } return nil @@ -2082,7 +2272,7 @@ func (s *ScopedKeyManager) toImportedPrivateManagedAddress( // // TODO: Handle imported key being part of internal branch. managedAddr, err := newManagedAddress( - s, ImportedDerivationPath, wif.PrivKey, wif.CompressPubKey, + s, ImportedDerivationPath, wif.PrivKey, nil, wif.CompressPubKey, s.addrSchema.ExternalAddrType, nil, ) if err != nil { @@ -2105,7 +2295,7 @@ func (s *ScopedKeyManager) toImportedPublicManagedAddress( // // TODO: Handle imported key being part of internal branch. managedAddr, err := newManagedAddressWithoutPrivKey( - s, ImportedDerivationPath, pubKey, compressed, + s, ImportedDerivationPath, pubKey, nil, compressed, s.addrSchema.ExternalAddrType, ) if err != nil { @@ -2198,21 +2388,24 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, witnessVersion byte, isSecretScript bool) (ManagedScriptAddress, error) { - s.mtx.Lock() - defer s.mtx.Unlock() + s.rootManager.mtx.Lock() + defer s.rootManager.mtx.Unlock() // The manager must be unlocked to encrypt the imported script. - if isSecretScript && s.rootManager.IsLocked() { + if isSecretScript && s.rootManager.isLocked() { return nil, managerError(ErrLocked, errLocked, nil) } // A secret script can only be used with a non-watch only manager. If // a wallet is watch-only then the script must be encrypted with the // public encryption key. - if isSecretScript && s.rootManager.WatchOnly() { + if isSecretScript && s.rootManager.watchOnly() { return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) } + s.mtx.Lock() + defer s.mtx.Unlock() + // Prevent duplicates. scriptIdent := identity() alreadyExists := s.existsAddress(ns, scriptIdent) @@ -2251,11 +2444,9 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, // The start block needs to be updated when the newly imported address // is before the current one. updateStartBlock := false - s.rootManager.mtx.Lock() if bs.Height < s.rootManager.syncState.startBlock.Height { updateStartBlock = true } - s.rootManager.mtx.Unlock() // Save the new imported address to the db and update start block (if // needed) in a single transaction. @@ -2287,9 +2478,7 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, // Now that the database has been updated, update the start block in // memory too if needed. if updateStartBlock { - s.rootManager.mtx.Lock() s.rootManager.syncState.startBlock = *bs - s.rootManager.mtx.Unlock() } // Create a new managed address based on the imported script. Also, @@ -2400,6 +2589,15 @@ func (s *ScopedKeyManager) LastAccount(ns walletdb.ReadBucket) (uint32, error) { func (s *ScopedKeyManager) ForEachAccountAddress(ns walletdb.ReadBucket, account uint32, fn func(maddr ManagedAddress) error) error { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + + return s.forEachAccountAddress(ns, account, fn) +} + +func (s *ScopedKeyManager) forEachAccountAddress(ns walletdb.ReadBucket, + account uint32, fn func(maddr ManagedAddress) error) error { + s.mtx.Lock() defer s.mtx.Unlock() @@ -2428,11 +2626,26 @@ func (s *ScopedKeyManager) ForEachActiveAccountAddress(ns walletdb.ReadBucket, a return s.ForEachAccountAddress(ns, account, fn) } +func (s *ScopedKeyManager) forEachActiveAccountAddress(ns walletdb.ReadBucket, account uint32, + fn func(maddr ManagedAddress) error) error { + + return s.forEachAccountAddress(ns, account, fn) +} + // ForEachActiveAddress calls the given function with each active address // stored in the manager, breaking early on error. func (s *ScopedKeyManager) ForEachActiveAddress(ns walletdb.ReadBucket, fn func(addr ltcutil.Address) error) error { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + + return s.forEachActiveAddress(ns, fn) +} + +func (s *ScopedKeyManager) forEachActiveAddress(ns walletdb.ReadBucket, + fn func(addr ltcutil.Address) error) error { + s.mtx.Lock() defer s.mtx.Unlock() @@ -2457,6 +2670,15 @@ func (s *ScopedKeyManager) ForEachActiveAddress(ns walletdb.ReadBucket, func (s *ScopedKeyManager) ForEachInternalActiveAddress(ns walletdb.ReadBucket, fn func(addr ltcutil.Address) error) error { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + + return s.forEachInternalActiveAddress(ns, fn) +} + +func (s *ScopedKeyManager) forEachInternalActiveAddress(ns walletdb.ReadBucket, + fn func(addr ltcutil.Address) error) error { + s.mtx.Lock() defer s.mtx.Unlock() @@ -2484,6 +2706,9 @@ func (s *ScopedKeyManager) ForEachInternalActiveAddress(ns walletdb.ReadBucket, func (s *ScopedKeyManager) IsWatchOnlyAccount(ns walletdb.ReadBucket, account uint32) (bool, error) { + s.rootManager.mtx.RLock() + defer s.rootManager.mtx.RUnlock() + s.mtx.Lock() defer s.mtx.Unlock() @@ -2509,7 +2734,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) ( switch net { case wire.MainNet: switch s.scope { - case KeyScopeBIP0044, KeyScopeBIP0086: + case KeyScopeBIP0044, KeyScopeBIP0086, KeyScopeMweb, KeyScopeLiteWallet: version = HDVersionMainNetBIP0044 case KeyScopeBIP0049Plus: version = HDVersionMainNetBIP0049 @@ -2523,7 +2748,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) ( netparams.SigNetWire(s.rootManager.ChainParams()): switch s.scope { - case KeyScopeBIP0044, KeyScopeBIP0086: + case KeyScopeBIP0044, KeyScopeBIP0086, KeyScopeMweb, KeyScopeLiteWallet: version = HDVersionTestNetBIP0044 case KeyScopeBIP0049Plus: version = HDVersionTestNetBIP0049 @@ -2535,7 +2760,7 @@ func (s *ScopedKeyManager) cloneKeyWithVersion(key *hdkeychain.ExtendedKey) ( case wire.SimNet: switch s.scope { - case KeyScopeBIP0044, KeyScopeBIP0086: + case KeyScopeBIP0044, KeyScopeBIP0086, KeyScopeMweb, KeyScopeLiteWallet: version = HDVersionSimNetBIP0044 // We use the mainnet versions for simnet keys when the keys // belong to a key scope which simnet doesn't have a defined diff --git a/waddrmgr/sync.go b/waddrmgr/sync.go index b4e99cf17a..d985cc2a5f 100644 --- a/waddrmgr/sync.go +++ b/waddrmgr/sync.go @@ -69,6 +69,13 @@ func (m *Manager) SetSyncedTo(ns walletdb.ReadWriteBucket, bs *BlockStamp) error return nil } +func (m *Manager) SetSyncedToCached(bs BlockStamp) { + m.mtx.Lock() + defer m.mtx.Unlock() + + m.syncState.syncedTo = bs +} + // SyncedTo returns details about the block height and hash that the address // manager is synced through at the very least. The intention is that callers // can use this information for intelligently initiating rescans to sync back to diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index 2b423f7ced..c933398e42 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -97,7 +97,7 @@ func (w *Wallet) handleChainNotifications() { // Sync may be interrupted by actions such as // locking the wallet. Try again after waiting a // bit. - err = w.syncWithChain(birthdayBlock) + birthdayBlock, err = w.syncWithChain(birthdayBlock) if err != nil { if w.ShuttingDown() { return ErrWalletShuttingDown @@ -190,6 +190,11 @@ func (w *Wallet) handleChainNotifications() { }) } notificationName = "filtered block connected" + case chain.MwebUtxos: + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + return w.checkMwebUtxos(tx, &n) + }) + notificationName = "mweb utxos" // The following require some database maintenance, but also // need to be reported to the wallet's rescan goroutine. @@ -319,6 +324,16 @@ func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + txDetails, _ := w.TxStore.TxDetails(txmgrNs, &rec.Hash) + if txDetails != nil { + rec = &txDetails.TxRecord + } + + err := w.extractCanonicalFromMweb(dbtx, rec) + if err != nil { + return err + } + // At the moment all notified transactions are assumed to actually be // relevant. This assumption will not hold true when SPV support is // added, but until then, simply insert the transaction because there @@ -336,6 +351,11 @@ func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, return nil } + isMwebPegout, _, err := w.getMwebPegouts(txmgrNs, rec) + if err != nil { + return err + } + // Check every output to determine whether it is controlled by a wallet // key. If so, mark the output as a credit. for i, output := range rec.MsgTx.TxOut { @@ -376,11 +396,13 @@ func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, // TODO: Credits should be added with the // account they belong to, so wtxmgr is able to // track per-account balances. - err = w.TxStore.AddCredit( - txmgrNs, rec, block, uint32(i), ma.Internal(), - ) - if err != nil { - return err + if !isMwebPegout[uint32(i)] { + err = w.TxStore.AddCredit( + txmgrNs, rec, block, uint32(i), ma.Internal(), + ) + if err != nil { + return err + } } err = w.Manager.MarkUsed(addrmgrNs, addr) if err != nil { @@ -421,6 +443,10 @@ func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, if details != nil { w.NtfnServer.notifyMinedTransaction(dbtx, details, block) } + + if nc, ok := w.ChainClient().(*chain.NeutrinoClient); ok { + nc.CS.MarkAsConfirmed(rec.Hash) + } } return nil diff --git a/wallet/common.go b/wallet/common.go index aa46f71514..d862a613b7 100644 --- a/wallet/common.go +++ b/wallet/common.go @@ -41,6 +41,7 @@ type OutputKind byte const ( OutputKindNormal OutputKind = iota OutputKindCoinbase + OutputKindMwebPegout ) // TransactionOutput describes an output that was or is at least partially diff --git a/wallet/createtx.go b/wallet/createtx.go index 78424afa02..33eacc464b 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -36,9 +36,10 @@ func makeInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { currentInputs := make([]*wire.TxIn, 0, len(eligible)) currentScripts := make([][]byte, 0, len(eligible)) currentInputValues := make([]ltcutil.Amount, 0, len(eligible)) + currentMwebOutputs := make([]*wire.MwebOutput, 0, len(eligible)) return func(target ltcutil.Amount) (ltcutil.Amount, []*wire.TxIn, - []ltcutil.Amount, [][]byte, error) { + []ltcutil.Amount, [][]byte, []*wire.MwebOutput, error) { for currentTotal < target && len(eligible) != 0 { nextCredit := &eligible[0] @@ -48,8 +49,10 @@ func makeInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { currentInputs = append(currentInputs, nextInput) currentScripts = append(currentScripts, nextCredit.PkScript) currentInputValues = append(currentInputValues, nextCredit.Amount) + currentMwebOutputs = append(currentMwebOutputs, nextCredit.MwebOutput) } - return currentTotal, currentInputs, currentInputValues, currentScripts, nil + return currentTotal, currentInputs, currentInputValues, + currentScripts, currentMwebOutputs, nil } } @@ -94,6 +97,18 @@ func (s secretSource) GetScript(addr ltcutil.Address) ([]byte, error) { return msa.Script() } +func (s secretSource) GetScanKey(addr ltcutil.Address) (*btcec.PrivateKey, error) { + sm, account, err := s.AddrAccount(s.addrmgrNs, addr) + if err != nil { + return nil, err + } + props, err := sm.AccountProperties(s.addrmgrNs, account) + if err != nil { + return nil, err + } + return props.AccountScanKey.ECPrivKey() +} + // txToOutputs creates a signed transaction which includes each output from // outputs. Previous outputs to redeem are chosen from the passed account's // UTXO set and minconf policy. An additional output may be added to return @@ -139,12 +154,41 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, return err } + allCanonical, allMweb := true, true + for _, txOut := range outputs { + if txscript.IsMweb(txOut.PkScript) { + allCanonical = false + } else { + allMweb = false + } + } + + var eligibleCanonical, eligibleMweb []wtxmgr.Credit + for _, credit := range eligible { + if txscript.IsMweb(credit.PkScript) { + eligibleMweb = append(eligibleMweb, credit) + } else { + eligibleCanonical = append(eligibleCanonical, credit) + } + } + var inputSource txauthor.InputSource switch coinSelectionStrategy { // Pick largest outputs first. case CoinSelectionLargest: - sort.Sort(sort.Reverse(byAmount(eligible))) + if allCanonical || allMweb { + sort.Sort(sort.Reverse(byAmount(eligibleCanonical))) + sort.Sort(sort.Reverse(byAmount(eligibleMweb))) + } + switch { + case allCanonical: + eligible = append(eligibleCanonical, eligibleMweb...) + case allMweb: + eligible = append(eligibleMweb, eligibleCanonical...) + default: + sort.Sort(sort.Reverse(byAmount(eligible))) + } inputSource = makeInputSource(eligible) // Select coins at random. This prevents the creation of ever @@ -154,7 +198,7 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, // Skip inputs that do not raise the total transaction // output value at the requested fee rate. var positivelyYielding []wtxmgr.Credit - for _, output := range eligible { + for _, output := range eligibleCanonical { output := output if !inputYieldsPositively(&output, feeSatPerKb) { @@ -166,10 +210,25 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, ) } - rand.Shuffle(len(positivelyYielding), func(i, j int) { - positivelyYielding[i], positivelyYielding[j] = - positivelyYielding[j], positivelyYielding[i] - }) + shuffle := func(arr []wtxmgr.Credit) { + rand.Shuffle(len(arr), func(i, j int) { + arr[i], arr[j] = arr[j], arr[i] + }) + } + + if allCanonical || allMweb { + shuffle(positivelyYielding) + shuffle(eligibleMweb) + } + switch { + case allCanonical: + positivelyYielding = append(positivelyYielding, eligibleMweb...) + case allMweb: + positivelyYielding = append(eligibleMweb, positivelyYielding...) + default: + positivelyYielding = append(positivelyYielding, eligibleMweb...) + shuffle(positivelyYielding) + } inputSource = makeInputSource(positivelyYielding) } @@ -208,7 +267,7 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, // (NP2WKH, P2WKH, P2TR), so any key scope provided // doesn't impact the result of this call. watchOnly, err = w.Manager.IsWatchOnlyAccount( - addrmgrNs, waddrmgr.KeyScopeBIP0086, account, + addrmgrNs, waddrmgr.KeyScopeBIP0084, account, ) } else { watchOnly, err = w.Manager.IsWatchOnlyAccount( @@ -219,9 +278,14 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, return err } if !watchOnly { - err = tx.AddAllInputScripts( - secretSource{w.Manager, addrmgrNs}, - ) + secrets := secretSource{w.Manager, addrmgrNs} + + err = tx.AddMweb(secrets, feeSatPerKb) + if err != nil { + return err + } + + err = tx.AddAllInputScripts(secrets) if err != nil { return err } @@ -295,11 +359,8 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, if !confirmed(minconf, output.Height, bs.Height) { continue } - if output.FromCoinBase { - target := int32(w.chainParams.CoinbaseMaturity) - if !confirmed(target, output.Height, bs.Height) { - continue - } + if !output.IsMature(bs.Height, w.chainParams) { + continue } // Locked unspent outputs are skipped. @@ -356,7 +417,7 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx, // Determine the address type for change addresses of the given // account. if changeKeyScope == nil { - changeKeyScope = &waddrmgr.KeyScopeBIP0086 + changeKeyScope = &waddrmgr.KeyScopeBIP0084 } addrType := waddrmgr.ScopeAddrMap[*changeKeyScope].InternalAddrType @@ -388,7 +449,7 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx, scriptSize = txsizes.P2TRPkScriptSize } - newChangeScript := func() ([]byte, error) { + newChangeScript := func(keyScope *waddrmgr.KeyScope) ([]byte, error) { // Derive the change output script. As a hack to allow spending // from the imported account, change addresses are created from // account 0. @@ -396,13 +457,16 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx, changeAddr ltcutil.Address err error ) + if keyScope == nil { + keyScope = changeKeyScope + } if account == waddrmgr.ImportedAddrAccount { changeAddr, err = w.newChangeAddress( - addrmgrNs, 0, *changeKeyScope, + addrmgrNs, 0, *keyScope, ) } else { changeAddr, err = w.newChangeAddress( - addrmgrNs, account, *changeKeyScope, + addrmgrNs, account, *keyScope, ) } if err != nil { diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index 0eb7bd91c5..96f6f88b58 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -351,8 +351,8 @@ func TestCreateSimpleCustomChange(t *testing.T) { PkScript: p2trScript, } tx1, err := w.txToOutputs( - []*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000, - CoinSelectionLargest, true, + []*wire.TxOut{targetTxOut}, nil, &waddrmgr.KeyScopeBIP0086, + 0, 1, 1000, CoinSelectionLargest, true, ) require.NoError(t, err) diff --git a/wallet/mweb.go b/wallet/mweb.go new file mode 100644 index 0000000000..53001196d7 --- /dev/null +++ b/wallet/mweb.go @@ -0,0 +1,532 @@ +package wallet + +import ( + "bytes" + "encoding/binary" + "errors" + "math/big" + "slices" + "time" + + "github.com/ltcsuite/ltcd/chaincfg/chainhash" + "github.com/ltcsuite/ltcd/ltcutil" + "github.com/ltcsuite/ltcd/ltcutil/mweb" + "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" + "github.com/ltcsuite/ltcd/wire" + "github.com/ltcsuite/ltcwallet/chain" + "github.com/ltcsuite/ltcwallet/internal/zero" + "github.com/ltcsuite/ltcwallet/waddrmgr" + "github.com/ltcsuite/ltcwallet/walletdb" + "github.com/ltcsuite/ltcwallet/wtxmgr" + "lukechampine.com/blake3" +) + +type ( + skmAccount struct { + skm *waddrmgr.ScopedKeyManager + account uint32 + } + mwebAccount struct { + skmAccount + scanSecret *mw.SecretKey + } +) + +func (w *Wallet) forEachMwebAccount(addrmgrNs walletdb.ReadBucket, + fn func(*mwebAccount) error) error { + + for _, scope := range w.Manager.ScopesForExternalAddrType(waddrmgr.Mweb) { + s, err := w.Manager.FetchScopedKeyManager(scope) + if err != nil { + return err + } + err = s.ForEachAccount(addrmgrNs, func(account uint32) error { + props, err := s.AccountProperties(addrmgrNs, account) + if err != nil || props.AccountScanKey == nil { + return err + } + scanKeyPriv, err := props.AccountScanKey.ECPrivKey() + if err != nil { + return err + } + defer scanKeyPriv.Zero() + scanSecret := (*mw.SecretKey)(scanKeyPriv.Serialize()) + defer zero.Bytes(scanSecret[:]) + return fn(&mwebAccount{skmAccount{s, account}, scanSecret}) + }) + if err != nil { + return err + } + } + + return nil +} + +func (w *Wallet) extractCanonicalFromMweb( + dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord) error { + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + + if rec.MsgTx.Mweb == nil { + return nil + } + + for _, input := range rec.MsgTx.Mweb.TxBody.Inputs { + op, txRec, err := w.TxStore.GetMwebOutpoint(txmgrNs, &input.OutputId) + switch { + case err != nil: + return err + case txRec != nil: + if !slices.ContainsFunc(rec.MsgTx.TxIn, func(txIn *wire.TxIn) bool { + return txIn.PreviousOutPoint == *op + }) { + rec.MsgTx.AddTxIn(&wire.TxIn{PreviousOutPoint: *op}) + } + } + } + + var outputs []*wire.MwebOutput + for _, output := range rec.MsgTx.Mweb.TxBody.Outputs { + op, _, err := w.TxStore.GetMwebOutpoint(txmgrNs, output.Hash()) + switch { + case err != nil: + return err + case op != nil: + if op.Hash != rec.Hash { + return errors.New("unexpected outpoint for output") + } + default: + outputs = append(outputs, output) + } + } + + err := w.forEachMwebAccount(addrmgrNs, func(ma *mwebAccount) error { + var remainingOutputs []*wire.MwebOutput + for _, output := range outputs { + coin, addr, err := w.rewindOutput(addrmgrNs, ma, output) + if err != nil { + return err + } else if coin == nil { + remainingOutputs = append(remainingOutputs, output) + continue + } + err = w.addMwebOutpoint(txmgrNs, rec, int64(coin.Value), + addr.ScriptAddress(), coin.OutputId) + if err != nil { + return err + } + } + outputs = remainingOutputs + return nil + }) + if err != nil { + return err + } + + _, pegouts, err := w.getMwebPegouts(txmgrNs, rec) + if err != nil { + return err + } + for hash, pegout := range pegouts { + err = w.addMwebOutpoint(txmgrNs, rec, + pegout.Value, pegout.PkScript, &hash) + if err != nil { + return err + } + } + + rec.SerializedTx = nil + + return nil +} + +func (w *Wallet) rewindOutput(addrmgrNs walletdb.ReadWriteBucket, + ma *mwebAccount, output *wire.MwebOutput) ( + coin *mweb.Coin, addr *ltcutil.AddressMweb, err error) { + + coin, err = mweb.RewindOutput(output, ma.scanSecret) + if err != nil { + return nil, nil, nil + } + addr = ltcutil.NewAddressMweb(coin.Address, w.chainParams) + ok, err := w.mwebKeyPools[ma.skmAccount].contains(addrmgrNs, addr) + if !ok { + coin = nil + } + return +} + +func (w *Wallet) addMwebOutpoint(txmgrNs walletdb.ReadWriteBucket, + rec *wtxmgr.TxRecord, value int64, script []byte, + outputId *chainhash.Hash) error { + + rec.MsgTx.AddTxOut(wire.NewTxOut(value, script)) + return w.TxStore.AddMwebOutpoint(txmgrNs, outputId, + wire.NewOutPoint(&rec.Hash, uint32(len(rec.MsgTx.TxOut)-1))) +} + +func (w *Wallet) getMwebPegouts( + txmgrNs walletdb.ReadBucket, rec *wtxmgr.TxRecord) ( + map[uint32]bool, map[chainhash.Hash]*wire.TxOut, error) { + + found := map[uint32]bool{} + missing := map[chainhash.Hash]*wire.TxOut{} + if rec.MsgTx.Mweb == nil { + return found, missing, nil + } + + for _, kernel := range rec.MsgTx.Mweb.TxBody.Kernels { + for i, pegout := range kernel.Pegouts { + h := blake3.New(32, nil) + h.Write(kernel.Hash()[:]) + binary.Write(h, binary.LittleEndian, uint32(i)) + hash := (*chainhash.Hash)(h.Sum(nil)) + op, _, err := w.TxStore.GetMwebOutpoint(txmgrNs, hash) + + switch { + case err != nil: + return nil, nil, err + case op != nil: + if op.Hash != rec.Hash { + return nil, nil, errors.New( + "unexpected outpoint for pegout") + } + found[op.Index] = true + default: + missing[*hash] = pegout + } + } + } + + return found, missing, nil +} + +func (w *Wallet) getBlockMeta(height int32) (*wtxmgr.BlockMeta, error) { + chainClient, err := w.requireChainClient() + if err != nil { + return nil, err + } + blockHash, err := chainClient.GetBlockHash(int64(height)) + if err != nil { + return nil, err + } + blockHeader, err := chainClient.GetBlockHeader(blockHash) + if err != nil { + return nil, err + } + return &wtxmgr.BlockMeta{ + Block: wtxmgr.Block{Hash: *blockHash, Height: height}, + Time: blockHeader.Timestamp, + }, nil +} + +func (w *Wallet) checkMwebUtxos( + dbtx walletdb.ReadWriteTx, n *chain.MwebUtxos) error { + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) + + type minedTx struct { + rec *wtxmgr.TxRecord + height int32 + } + minedTxns := make(map[chainhash.Hash]minedTx) + var utxos []*wire.MwebNetUtxo + + for _, utxo := range n.Utxos { + _, rec, err := w.TxStore.GetMwebOutpoint(txmgrNs, utxo.OutputId) + switch { + case err != nil: + return err + case rec != nil && rec.MsgTx.Mweb != nil: + minedTxns[rec.Hash] = minedTx{rec, utxo.Height} + default: + utxos = append(utxos, utxo) + } + } + + for _, tx := range minedTxns { + block, err := w.getBlockMeta(tx.height) + if err != nil { + return err + } + err = w.addRelevantTx(dbtx, tx.rec, block) + if err != nil { + return err + } + if block.Height <= w.Manager.SyncedTo().Height { + w.NtfnServer.notifyAttachedBlock(dbtx, block) + } + } + + err := w.forEachMwebAccount(addrmgrNs, func(ma *mwebAccount) error { + var remainingUtxos []*wire.MwebNetUtxo + for _, utxo := range utxos { + coin, addr, err := w.rewindOutput(addrmgrNs, ma, utxo.Output) + if err != nil { + return err + } else if coin == nil { + remainingUtxos = append(remainingUtxos, utxo) + continue + } + block, err := w.getBlockMeta(utxo.Height) + if err != nil { + return err + } + rec := &wtxmgr.TxRecord{ + MsgTx: wire.MsgTx{ + Mweb: &wire.MwebTx{TxBody: &wire.MwebTxBody{ + Outputs: []*wire.MwebOutput{utxo.Output}, + Kernels: []*wire.MwebKernel{{}}, + }}, + }, + Hash: *utxo.OutputId, + Received: block.Time, + } + if utxo.Height == 0 { + rec.Received = time.Now() + block = nil + } + err = w.addMwebOutpoint(txmgrNs, rec, int64(coin.Value), + addr.ScriptAddress(), coin.OutputId) + if err != nil { + return err + } + err = w.addRelevantTx(dbtx, rec, block) + if err != nil { + return err + } + if block != nil && block.Height <= w.Manager.SyncedTo().Height { + w.NtfnServer.notifyAttachedBlock(dbtx, block) + } + } + utxos = remainingUtxos + return nil + }) + if err != nil { + return err + } + + return w.checkMwebLeafset(dbtx, n.Leafset) +} + +func (w *Wallet) checkMwebLeafset(dbtx walletdb.ReadWriteTx, + newLeafset *mweb.Leafset) error { + + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + + if newLeafset == nil || newLeafset.Block == nil { + return nil + } + + oldLeafset, err := w.getMwebLeafset(addrmgrNs) + if err != nil { + return err + } + + err = w.putMwebLeafset(addrmgrNs, newLeafset) + if err != nil { + return err + } + + oldBits := oldLeafset.Bits + newBits := newLeafset.Bits + if len(oldBits) < len(newBits) { + newBits = newBits[:len(oldBits)] + } else { + oldBits = oldBits[:len(newBits)] + } + + old := new(big.Int).SetBytes(oldBits) + new := new(big.Int).SetBytes(newBits) + + if new.And(old, new).Cmp(old) == 0 { + return nil + } + + // Leaves in the old leafset were spent, check if they were ours. + chainClient, err := w.requireChainClient() + if err != nil { + return err + } + + nc, ok := chainClient.(*chain.NeutrinoClient) + if !ok { + return nil + } + + outputs, err := w.TxStore.UnspentOutputs(txmgrNs) + if err != nil { + return err + } + + var rec wtxmgr.TxRecord + for _, output := range outputs { + if output.MwebOutput == nil || output.Height < 0 { + continue + } + if !nc.CS.MwebUtxoExists(output.MwebOutput.Hash()) { + rec.MsgTx.AddTxIn(&wire.TxIn{PreviousOutPoint: output.OutPoint}) + } + } + + if len(rec.MsgTx.TxIn) == 0 { + return nil + } + + block := &wtxmgr.BlockMeta{ + Block: wtxmgr.Block{ + Hash: newLeafset.Block.BlockHash(), + Height: int32(newLeafset.Height), + }, + Time: newLeafset.Block.Timestamp, + } + + rec.Hash = rec.MsgTx.TxHash() + rec.Received = block.Time + + err = w.addRelevantTx(dbtx, &rec, block) + if err != nil { + return err + } + + if block.Height <= w.Manager.SyncedTo().Height { + w.NtfnServer.notifyAttachedBlock(dbtx, block) + } + + return nil +} + +var mwebLeafsets = []byte("mwebLeafsets") + +func (w *Wallet) getMwebLeafset( + addrmgrNs walletdb.ReadBucket) (*mweb.Leafset, error) { + + leafset := &mweb.Leafset{} + mwebLeafsets := addrmgrNs.NestedReadBucket(mwebLeafsets) + if mwebLeafsets == nil { + return leafset, nil + } + + err := mwebLeafsets.ForEach(func(k, v []byte) error { + lfs := &mweb.Leafset{} + err := lfs.Deserialize(bytes.NewReader(v)) + if err != nil { + return err + } + + hash, err := w.Manager.BlockHash(addrmgrNs, int32(lfs.Height)) + + switch { + case lfs.Height < leafset.Height: + case lfs.Height > uint32(w.Manager.SyncedTo().Height): + case waddrmgr.IsError(err, waddrmgr.ErrBlockNotFound): + case err != nil: + return err + case lfs.Block.BlockHash() == *hash: + leafset = lfs + } + + return nil + }) + + return leafset, err +} + +func (w *Wallet) putMwebLeafset(addrmgrNs walletdb.ReadWriteBucket, + leafset *mweb.Leafset) error { + + mwebLeafsets, err := addrmgrNs.CreateBucketIfNotExists(mwebLeafsets) + if err != nil { + return err + } + + // Delete older and newer leafsets. + err = mwebLeafsets.ForEach(func(k, v []byte) error { + height := binary.LittleEndian.Uint32(k) + if height < leafset.Height-10 || height > leafset.Height { + return mwebLeafsets.Delete(k) + } + return nil + }) + if err != nil { + return err + } + + var buf bytes.Buffer + if err = leafset.Serialize(&buf); err != nil { + return err + } + k := binary.LittleEndian.AppendUint32(nil, leafset.Height) + return mwebLeafsets.Put(k, buf.Bytes()) +} + +type mwebKeyPool struct { + mwebAccount + index uint32 + keychain *mweb.Keychain + addrs []*mw.StealthAddress +} + +func newMwebKeyPool(addrmgrNs walletdb.ReadBucket, + ma *mwebAccount) (*mwebKeyPool, error) { + + props, err := ma.skm.AccountProperties(addrmgrNs, ma.account) + if err != nil { + return nil, err + } + + spendPubKey, err := props.AccountSpendPubKey.ECPubKey() + if err != nil { + return nil, err + } + + kp := &mwebKeyPool{ + mwebAccount: *ma, + index: props.ExternalKeyCount, + keychain: &mweb.Keychain{ + Scan: (*mw.SecretKey)(bytes.Clone(ma.scanSecret[:])), + SpendPubKey: (*mw.PublicKey)(spendPubKey.SerializeCompressed()), + }, + } + kp.topUp() + + return kp, nil +} + +func (kp *mwebKeyPool) topUp() { + for len(kp.addrs) < 1000 { + index := kp.index + uint32(len(kp.addrs)) + kp.addrs = append(kp.addrs, kp.keychain.Address(index)) + } +} + +func (kp *mwebKeyPool) contains(addrmgrNs walletdb.ReadWriteBucket, + addr *ltcutil.AddressMweb) (bool, error) { + + switch _, err := kp.skm.Address(addrmgrNs, addr); { + case err == nil: + return true, nil + case !waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound): + return false, err + } + + index := slices.IndexFunc(kp.addrs, addr.StealthAddress().Equal) + if index < 0 { + return false, nil + } + + err := kp.skm.ExtendExternalAddresses(addrmgrNs, + kp.account, kp.index+uint32(index)) + if err != nil { + return false, err + } + + kp.index += uint32(index + 1) + kp.addrs = kp.addrs[index+1:] + kp.topUp() + + return true, nil +} diff --git a/wallet/notifications.go b/wallet/notifications.go index 48a0959b1e..efe2224c00 100644 --- a/wallet/notifications.go +++ b/wallet/notifications.go @@ -106,13 +106,19 @@ func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails) T serializedTx = buf.Bytes() } var fee ltcutil.Amount - if len(details.Debits) == len(details.MsgTx.TxIn) { + nDebits := len(details.Debits) + if nDebits > 0 && nDebits == len(details.MsgTx.TxIn) { for _, deb := range details.Debits { fee += deb.Amount } for _, txOut := range details.MsgTx.TxOut { fee -= ltcutil.Amount(txOut.Value) } + if details.MsgTx.Mweb != nil { + for _, kernel := range details.MsgTx.Mweb.TxBody.Kernels { + fee += ltcutil.Amount(kernel.Pegin) + } + } } var inputs []TransactionSummaryInput if len(details.Debits) != 0 { @@ -143,6 +149,7 @@ func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails) T return TransactionSummary{ Hash: &details.Hash, Transaction: serializedTx, + Broadcast: details.BroadcastTx, MyInputs: inputs, MyOutputs: outputs, Fee: fee, @@ -361,6 +368,7 @@ type Block struct { type TransactionSummary struct { Hash *chainhash.Hash Transaction []byte + Broadcast []byte MyInputs []TransactionSummaryInput MyOutputs []TransactionSummaryOutput Fee ltcutil.Amount diff --git a/wallet/psbt.go b/wallet/psbt.go index 82772c58d2..398be3e594 100644 --- a/wallet/psbt.go +++ b/wallet/psbt.go @@ -556,6 +556,7 @@ func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { currentInputs := make([]*wire.TxIn, 0, len(eligible)) currentScripts := make([][]byte, 0, len(eligible)) currentInputValues := make([]ltcutil.Amount, 0, len(eligible)) + currentMwebOutputs := make([]*wire.MwebOutput, 0, len(eligible)) for _, credit := range eligible { nextInput := wire.NewTxIn(&credit.OutPoint, nil, nil) @@ -563,12 +564,13 @@ func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { currentInputs = append(currentInputs, nextInput) currentScripts = append(currentScripts, credit.PkScript) currentInputValues = append(currentInputValues, credit.Amount) + currentMwebOutputs = append(currentMwebOutputs, credit.MwebOutput) } return func(target ltcutil.Amount) (ltcutil.Amount, []*wire.TxIn, - []ltcutil.Amount, [][]byte, error) { + []ltcutil.Amount, [][]byte, []*wire.MwebOutput, error) { return currentTotal, currentInputs, currentInputValues, - currentScripts, nil + currentScripts, currentMwebOutputs, nil } } diff --git a/wallet/txauthor/author.go b/wallet/txauthor/author.go index 970d3439c7..9f04f0f4ab 100644 --- a/wallet/txauthor/author.go +++ b/wallet/txauthor/author.go @@ -7,11 +7,17 @@ package txauthor import ( "errors" + "slices" + "github.com/ltcsuite/ltcd/btcec/v2" "github.com/ltcsuite/ltcd/chaincfg" "github.com/ltcsuite/ltcd/ltcutil" + "github.com/ltcsuite/ltcd/ltcutil/mweb" + "github.com/ltcsuite/ltcd/ltcutil/mweb/mw" "github.com/ltcsuite/ltcd/txscript" "github.com/ltcsuite/ltcd/wire" + "github.com/ltcsuite/ltcwallet/internal/zero" + "github.com/ltcsuite/ltcwallet/waddrmgr" "github.com/ltcsuite/ltcwallet/wallet/txrules" "github.com/ltcsuite/ltcwallet/wallet/txsizes" ) @@ -29,8 +35,9 @@ func SumOutputValues(outputs []*wire.TxOut) (totalOutput ltcutil.Amount) { // can not be satisified, this can be signaled by returning a total amount less // than the target or by returning a more detailed error implementing // InputSourceError. -type InputSource func(target ltcutil.Amount) (total ltcutil.Amount, inputs []*wire.TxIn, - inputValues []ltcutil.Amount, scripts [][]byte, err error) +type InputSource func(target ltcutil.Amount) (total ltcutil.Amount, + inputs []*wire.TxIn, inputValues []ltcutil.Amount, scripts [][]byte, + mwebOutputs []*wire.MwebOutput, err error) // InputSourceError describes the failure to provide enough input value from // unspent transaction outputs to meet a target amount. A typed error is used @@ -56,15 +63,17 @@ type AuthoredTx struct { Tx *wire.MsgTx PrevScripts [][]byte PrevInputValues []ltcutil.Amount + PrevMwebOutputs []*wire.MwebOutput TotalInput ltcutil.Amount ChangeIndex int // negative if no change + NewMwebCoins []*mweb.Coin } // ChangeSource provides change output scripts for transaction creation. type ChangeSource struct { // NewScript is a closure that produces unique change output scripts per // invocation. - NewScript func() ([]byte, error) + NewScript func(*waddrmgr.KeyScope) ([]byte, error) // ScriptSize is the size in bytes of scripts produced by `NewScript`. ScriptSize int @@ -99,8 +108,17 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb ltcutil.Amount, ) targetFee := txrules.FeeForSerializeSize(feeRatePerKb, estimatedSize) + mwebFee := mweb.EstimateFee(outputs, feeRatePerKb, true) + isMweb := slices.ContainsFunc(outputs, func(txOut *wire.TxOut) bool { + return txscript.IsMweb(txOut.PkScript) + }) + if isMweb { + targetFee = ltcutil.Amount(mwebFee) + } + for { - inputAmount, inputs, inputValues, scripts, err := fetchInputs(targetAmount + targetFee) + inputAmount, inputs, inputValues, scripts, mwebOutputs, + err := fetchInputs(targetAmount + targetFee) if err != nil { return nil, err } @@ -110,7 +128,7 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb ltcutil.Amount, // We count the types of inputs, which we'll use to estimate // the vsize of the transaction. - var nested, p2wpkh, p2tr, p2pkh int + var nested, p2wpkh, p2tr, mwebIn, p2pkh int for _, pkScript := range scripts { switch { // If this is a p2sh output, we assume this is a @@ -121,15 +139,40 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb ltcutil.Amount, p2wpkh++ case txscript.IsPayToTaproot(pkScript): p2tr++ + case txscript.IsMweb(pkScript): + mwebIn++ default: p2pkh++ } } + isMweb := isMweb || mwebIn > 0 + outputsToEstimate := outputs + changeScriptSize := changeSource.ScriptSize + if isMweb && mwebIn < len(inputs) { + outputsToEstimate = []*wire.TxOut{mweb.NewPegin( + uint64(inputAmount), &wire.MwebKernel{})} + changeScriptSize = 0 + } + maxSignedSize := txsizes.EstimateVirtualSize( - p2pkh, p2tr, p2wpkh, nested, outputs, changeSource.ScriptSize, + p2pkh, p2tr, p2wpkh, nested, outputsToEstimate, changeScriptSize, ) + if isMweb && mwebIn < len(inputs) { + maxSignedSize += new(wire.TxIn).SerializeSize() + } maxRequiredFee := txrules.FeeForSerializeSize(feeRatePerKb, maxSignedSize) + + var changeKeyScope *waddrmgr.KeyScope + if isMweb { + if mwebIn < len(inputs) { + maxRequiredFee += ltcutil.Amount(mwebFee) + } else { + maxRequiredFee = ltcutil.Amount(mwebFee) + } + changeKeyScope = &waddrmgr.KeyScopeMweb + } + remainingAmount := inputAmount - targetAmount if remainingAmount < maxRequiredFee { targetFee = maxRequiredFee @@ -145,13 +188,13 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb ltcutil.Amount, changeIndex := -1 changeAmount := inputAmount - targetAmount - maxRequiredFee - changeScript, err := changeSource.NewScript() + changeScript, err := changeSource.NewScript(changeKeyScope) if err != nil { return nil, err } change := wire.NewTxOut(int64(changeAmount), changeScript) if changeAmount != 0 && !txrules.IsDustOutput(change, - txrules.DefaultRelayFeePerKb) { + txrules.DefaultRelayFeePerKb) || isMweb { l := len(outputs) unsignedTransaction.TxOut = append(outputs[:l:l], change) @@ -162,6 +205,7 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb ltcutil.Amount, Tx: unsignedTransaction, PrevScripts: scripts, PrevInputValues: inputValues, + PrevMwebOutputs: mwebOutputs, TotalInput: inputAmount, ChangeIndex: changeIndex, }, nil @@ -198,6 +242,7 @@ type SecretsSource interface { txscript.KeyDB txscript.ScriptDB ChainParams() *chaincfg.Params + GetScanKey(ltcutil.Address) (*btcec.PrivateKey, error) } // AddAllInputScripts modifies transaction a transaction by adding inputs @@ -457,3 +502,104 @@ func TXPrevOutFetcher(tx *wire.MsgTx, prevPkScripts [][]byte, return fetcher, nil } + +func (tx *AuthoredTx) AddMweb(secrets SecretsSource, + feeRatePerKb ltcutil.Amount) (err error) { + + var ( + chainParams = secrets.ChainParams() + txIns []*wire.TxIn + pegouts []*wire.TxOut + coins []*mweb.Coin + recipients []*mweb.Recipient + prevScripts [][]byte + prevValues []ltcutil.Amount + sumCoins uint64 + sumOutputs uint64 + ) + + for i, txIn := range tx.Tx.TxIn { + if !txscript.IsMweb(tx.PrevScripts[i]) { + txIns = append(txIns, txIn) + prevScripts = append(prevScripts, tx.PrevScripts[i]) + prevValues = append(prevValues, tx.PrevInputValues[i]) + continue + } + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + tx.PrevScripts[i], chainParams) + if err != nil { + return err + } + scanKeyPriv, err := secrets.GetScanKey(addrs[0]) + if err != nil { + return err + } + defer scanKeyPriv.Zero() + scanSecret := (*mw.SecretKey)(scanKeyPriv.Serialize()) + defer zero.Bytes(scanSecret[:]) + + coin, err := mweb.RewindOutput(tx.PrevMwebOutputs[i], scanSecret) + if err != nil { + return err + } + coins = append(coins, coin) + sumCoins += coin.Value + + spendKeyPriv, _, err := secrets.GetKey(addrs[0]) + if err != nil { + return err + } + defer spendKeyPriv.Zero() + spendSecret := (*mw.SecretKey)(spendKeyPriv.Serialize()) + defer zero.Bytes(spendSecret[:]) + + coin.CalculateOutputKey(spendSecret) + } + + for _, txOut := range tx.Tx.TxOut { + sumOutputs += uint64(txOut.Value) + if !txscript.IsMweb(txOut.PkScript) { + pegouts = append(pegouts, txOut) + continue + } + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + txOut.PkScript, chainParams) + if err != nil { + return err + } + recipients = append(recipients, &mweb.Recipient{ + Value: uint64(txOut.Value), + Address: addrs[0].(*ltcutil.AddressMweb).StealthAddress(), + }) + } + + if len(coins) == 0 && len(recipients) == 0 { + return + } + + var fee, pegin uint64 + if len(txIns) > 0 { + fee = mweb.EstimateFee(tx.Tx.TxOut, feeRatePerKb, false) + pegin = sumOutputs + fee - sumCoins + } else { + fee = sumCoins - sumOutputs + } + + tx.Tx.Mweb, tx.NewMwebCoins, err = + mweb.NewTransaction(coins, recipients, fee, pegin, pegouts) + if err != nil { + return err + } + + tx.Tx.TxIn = txIns + tx.Tx.TxOut = nil + tx.PrevScripts = prevScripts + tx.PrevInputValues = prevValues + tx.ChangeIndex = -1 + + if pegin > 0 { + tx.Tx.AddTxOut(mweb.NewPegin(pegin, tx.Tx.Mweb.TxBody.Kernels[0])) + } + + return +} diff --git a/wallet/txauthor/author_test.go b/wallet/txauthor/author_test.go index 847653b0c8..f9298f734a 100644 --- a/wallet/txauthor/author_test.go +++ b/wallet/txauthor/author_test.go @@ -9,6 +9,7 @@ import ( "github.com/ltcsuite/ltcd/ltcutil" "github.com/ltcsuite/ltcd/wire" + "github.com/ltcsuite/ltcwallet/waddrmgr" "github.com/ltcsuite/ltcwallet/wallet/txrules" "github.com/ltcsuite/ltcwallet/wallet/txsizes" ) @@ -27,7 +28,8 @@ func makeInputSource(unspents []*wire.TxOut) InputSource { currentTotal := ltcutil.Amount(0) currentInputs := make([]*wire.TxIn, 0, len(unspents)) currentInputValues := make([]ltcutil.Amount, 0, len(unspents)) - f := func(target ltcutil.Amount) (ltcutil.Amount, []*wire.TxIn, []ltcutil.Amount, [][]byte, error) { + f := func(target ltcutil.Amount) (ltcutil.Amount, []*wire.TxIn, + []ltcutil.Amount, [][]byte, []*wire.MwebOutput, error) { for currentTotal < target && len(unspents) != 0 { u := unspents[0] unspents = unspents[1:] @@ -36,7 +38,8 @@ func makeInputSource(unspents []*wire.TxOut) InputSource { currentInputs = append(currentInputs, nextInput) currentInputValues = append(currentInputValues, ltcutil.Amount(u.Value)) } - return currentTotal, currentInputs, currentInputValues, make([][]byte, len(currentInputs)), nil + return currentTotal, currentInputs, currentInputValues, + make([][]byte, len(currentInputs)), nil, nil } return InputSource(f) } @@ -175,7 +178,7 @@ func TestNewUnsignedTransaction(t *testing.T) { } changeSource := &ChangeSource{ - NewScript: func() ([]byte, error) { + NewScript: func(*waddrmgr.KeyScope) ([]byte, error) { // Only length matters for these tests. return make([]byte, txsizes.P2WPKHPkScriptSize), nil }, diff --git a/wallet/txauthor/go.mod b/wallet/txauthor/go.mod index d1776949ee..8809b87d72 100644 --- a/wallet/txauthor/go.mod +++ b/wallet/txauthor/go.mod @@ -1,12 +1,3 @@ module github.com/ltcsuite/ltcwallet/wallet/txauthor -go 1.12 - -require ( - github.com/ltcsuite/ltcd v0.23.5 - github.com/ltcsuite/ltcd/ltcutil v1.1.3 - github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 - github.com/ltcsuite/ltcwallet/wallet/txsizes v1.2.3 -) - -replace github.com/ltcsuite/ltcwallet/wallet/txsizes => ../txsizes +go 1.18 diff --git a/wallet/txauthor/go.sum b/wallet/txauthor/go.sum index 1bba84deab..e69de29bb2 100644 --- a/wallet/txauthor/go.sum +++ b/wallet/txauthor/go.sum @@ -1,263 +0,0 @@ -github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= -github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= -github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= -github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= -github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= -github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ltcsuite/ltcd v0.22.0-beta/go.mod h1:/BXtm50r591uMfXf8XgSpL5er32HCvheJtBSPYK5bFM= -github.com/ltcsuite/ltcd v0.23.5 h1:MFWjmx2hCwxrUu9v0wdIPOSN7PHg9BWQeh+AO4FsVLI= -github.com/ltcsuite/ltcd v0.23.5/go.mod h1:JV6swXR5m0cYFi0VYdQPp3UnMdaDQxaRUCaU1PPjb+g= -github.com/ltcsuite/ltcd/btcec/v2 v2.1.0/go.mod h1:Vc9ZYXMcl5D6bA0VwMvGRDJYggO3YZ7/BuIri02Lq0E= -github.com/ltcsuite/ltcd/btcec/v2 v2.3.2 h1:HVArUNQGqGaSSoyYkk9qGht74U0/uNhS0n7jV9rkmno= -github.com/ltcsuite/ltcd/btcec/v2 v2.3.2/go.mod h1:T1t5TjbjPnryvlGQ+RpSKGuU8KhjNN7rS5+IznPj1VM= -github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 h1:xuWxvRKxLvOKuS7/Q/7I3tpc3cWAB0+hZpU8YdVqkzg= -github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2/go.mod h1:nkLkAFGhursWf2U68gt61hPieK1I+0m78e+2aevNyD8= -github.com/ltcsuite/ltcd/ltcutil v1.1.0/go.mod h1:VbZlcopVgQteiCC5KRjIuxXH5wi1CtzhsvoYZ3K7FaE= -github.com/ltcsuite/ltcd/ltcutil v1.1.3 h1:8AapjCPLIt/wtYe6Odfk1EC2y9mcbpgjyxyCoNjAkFI= -github.com/ltcsuite/ltcd/ltcutil v1.1.3/go.mod h1:z8txd/ohBFrOMBUT70K8iZvHJD/Vc3gzx+6BP6cBxQw= -github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0 h1:P6H9zsMpBBuGOsp9lnil7XfPaPujDqrbcmkqvDdiSiI= -github.com/ltcsuite/ltcwallet/wallet/txrules v1.2.0/go.mod h1:lmA2Ozxvbr2M8Mqb6ugOv5/FQT6x2Qnwg3yT/NiWEks= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= -github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= -github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= -github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= -github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= -github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= -github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= -github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= -github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= -github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= -github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= -github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= -github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= -lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/wallet/txrules/go.mod b/wallet/txrules/go.mod index 0e156fad2a..0c991e0584 100644 --- a/wallet/txrules/go.mod +++ b/wallet/txrules/go.mod @@ -8,9 +8,10 @@ require ( github.com/ltcsuite/ltcd v0.23.5 github.com/ltcsuite/ltcd/ltcutil v1.1.3 github.com/onsi/gomega v1.26.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect - github.com/stretchr/testify v1.8.2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/testify v1.8.3 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/wallet/txrules/go.sum b/wallet/txrules/go.sum index 00691da899..87a7e3cb65 100644 --- a/wallet/txrules/go.sum +++ b/wallet/txrules/go.sum @@ -95,22 +95,19 @@ github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2 github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -184,8 +181,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/wallet/txsizes/go.mod b/wallet/txsizes/go.mod index 547e2a5b85..439bf68c46 100644 --- a/wallet/txsizes/go.mod +++ b/wallet/txsizes/go.mod @@ -7,9 +7,10 @@ require ( github.com/kr/pretty v0.3.0 // indirect github.com/ltcsuite/ltcd v0.23.5 github.com/onsi/gomega v1.26.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect - github.com/stretchr/testify v1.8.2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/testify v1.8.3 // indirect golang.org/x/crypto v0.7.0 // indirect golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/wallet/txsizes/go.sum b/wallet/txsizes/go.sum index e67995f242..774c87e16c 100644 --- a/wallet/txsizes/go.sum +++ b/wallet/txsizes/go.sum @@ -93,22 +93,19 @@ github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2 github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -182,8 +179,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/wallet/txsizes/size.go b/wallet/txsizes/size.go index 3f798191e6..c06ea29e22 100644 --- a/wallet/txsizes/size.go +++ b/wallet/txsizes/size.go @@ -218,7 +218,7 @@ func EstimateVirtualSize(numP2PKHIns, numP2TRIns, numP2WPKHIns, numNestedP2WPKHI baseSize := 8 + wire.VarIntSerializeSize( uint64(numP2PKHIns+numP2TRIns+numP2WPKHIns+numNestedP2WPKHIns)) + - wire.VarIntSerializeSize(uint64(len(txOuts))) + + wire.VarIntSerializeSize(uint64(outputCount)) + numP2PKHIns*RedeemP2PKHInputSize + numP2WPKHIns*RedeemP2WPKHInputSize + numP2TRIns*RedeemP2TRInputSize + diff --git a/wallet/utxos.go b/wallet/utxos.go index 20c0259289..10d6af7d6c 100644 --- a/wallet/utxos.go +++ b/wallet/utxos.go @@ -82,6 +82,8 @@ func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOut outputSource := OutputKindNormal if output.FromCoinBase { outputSource = OutputKindCoinbase + } else if output.IsMwebPegout { + outputSource = OutputKindMwebPegout } result := &TransactionOutput{ diff --git a/wallet/wallet.go b/wallet/wallet.go index 64e11dffe0..66c4cafeef 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -24,6 +24,7 @@ import ( "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil" "github.com/ltcsuite/ltcd/ltcutil/hdkeychain" + "github.com/ltcsuite/ltcd/ltcutil/mweb" "github.com/ltcsuite/ltcd/txscript" "github.com/ltcsuite/ltcd/wire" "github.com/ltcsuite/ltcwallet/chain" @@ -156,6 +157,8 @@ type Wallet struct { // syncRetryInterval is the amount of time to wait between re-tries on // errors during initial sync. syncRetryInterval time.Duration + + mwebKeyPools map[skmAccount]*mwebKeyPool } // Start starts the goroutines necessary to manage a wallet. @@ -363,7 +366,7 @@ func (w *Wallet) activeData(dbtx walletdb.ReadWriteTx) ([]ltcutil.Address, []wtx return nil, nil, err } - unspent, err := w.TxStore.UnspentOutputs(txmgrNs) + unspent, err := w.TxStore.UnspentOutputs2(txmgrNs, true) return addrs, unspent, err } @@ -371,10 +374,12 @@ func (w *Wallet) activeData(dbtx walletdb.ReadWriteTx) ([]ltcutil.Address, []wtx // connection. It creates a rescan request and blocks until the rescan has // finished. The birthday block can be passed in, if set, to ensure we can // properly detect if it gets rolled back. -func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { +func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) ( + *waddrmgr.BlockStamp, error) { + chainClient, err := w.requireChainClient() if err != nil { - return err + return birthdayStamp, err } // Neutrino relies on the information given to it by the cfheader server @@ -394,7 +399,7 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { if !w.isDevEnv() || neutrinoRecovery { log.Debug("Waiting for chain backend to sync to tip") if err := w.waitUntilBackendSynced(chainClient); err != nil { - return err + return birthdayStamp, err } log.Debug("Chain backend synced to tip!") } @@ -406,8 +411,8 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { chainClient, w.Manager.Birthday(), ) if err != nil { - return fmt.Errorf("unable to locate birthday block: %v", - err) + return birthdayStamp, fmt.Errorf( + "unable to locate birthday block: %v", err) } // We'll also determine our initial sync starting height. This @@ -421,11 +426,11 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { // details required by the wallet. startHash, err := chainClient.GetBlockHash(int64(startHeight)) if err != nil { - return err + return birthdayStamp, err } startHeader, err := chainClient.GetBlockHeader(startHash) if err != nil { - return err + return birthdayStamp, err } err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { @@ -441,8 +446,8 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { return w.Manager.SetBirthdayBlock(ns, *birthdayStamp, true) }) if err != nil { - return fmt.Errorf("unable to persist initial sync "+ - "data: %v", err) + return birthdayStamp, fmt.Errorf( + "unable to persist initial sync data: %v", err) } } @@ -450,8 +455,8 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { // so now. if w.recoveryWindow > 0 { if err := w.recovery(chainClient, birthdayStamp); err != nil { - return fmt.Errorf("unable to perform wallet recovery: "+ - "%v", err) + return birthdayStamp, fmt.Errorf( + "unable to perform wallet recovery: %v", err) } } @@ -520,7 +525,7 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { return w.TxStore.Rollback(txmgrNs, rollbackStamp.Height+1) }) if err != nil { - return err + return birthdayStamp, err } // Request notifications for connected and disconnected blocks. @@ -532,7 +537,7 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { // notification re-registrations, in which case the code here should be // left as is. if err := chainClient.NotifyBlocks(); err != nil { - return err + return birthdayStamp, err } // Finally, we'll trigger a wallet rescan and request notifications for @@ -541,16 +546,42 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { var ( addrs []ltcutil.Address unspent []wtxmgr.Credit + leafset *mweb.Leafset ) err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { + addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) + + err = w.forEachMwebAccount(addrmgrNs, func(ma *mwebAccount) error { + kp, err := newMwebKeyPool(addrmgrNs, ma) + if err != nil { + return err + } + w.mwebKeyPools[ma.skmAccount] = kp + return nil + }) + if err != nil { + return err + } + + leafset, err = w.getMwebLeafset(addrmgrNs) + if err != nil { + return err + } + addrs, unspent, err = w.activeData(dbtx) return err }) if err != nil { - return err + return birthdayStamp, err } - return w.rescanWithTarget(addrs, unspent, nil) + if s, ok := chainClient.(*chain.NeutrinoClient); ok { + if err = s.CS.NotifyAddedMwebUtxos(leafset); err != nil { + return birthdayStamp, err + } + } + + return birthdayStamp, w.rescanWithTarget(addrs, unspent, nil) } // isDevEnv determines whether the wallet is currently under a local developer @@ -736,7 +767,7 @@ func (w *Wallet) recovery(chainClient chain.Interface, var blocks []*waddrmgr.BlockStamp startHeight := w.Manager.SyncedTo().Height + 1 for height := startHeight; height <= bestHeight; height++ { - if atomic.LoadUint32(&syncer.quit) == 1 { + if atomic.LoadUint32(&syncer.quit) == 1 || w.ShuttingDown() { return errors.New("recovery: forced shutdown") } @@ -769,6 +800,7 @@ func (w *Wallet) recovery(chainClient chain.Interface, // state to disk. recoveryBatch := recoveryMgr.BlockBatch() if len(recoveryBatch) == recoveryBatchSize || height == bestHeight { + syncedTo := w.Manager.SyncedTo() err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) for _, block := range blocks { @@ -783,6 +815,7 @@ func (w *Wallet) recovery(chainClient chain.Interface, ) }) if err != nil { + w.Manager.SetSyncedToCached(syncedTo) return err } @@ -1627,8 +1660,7 @@ func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (Balan } bals.Total += output.Amount - if output.FromCoinBase && !confirmed(int32(w.chainParams.CoinbaseMaturity), - output.Height, syncBlock.Height) { + if !output.IsMature(syncBlock.Height, w.chainParams) { bals.ImmatureReward += output.Amount } else if confirmed(confirms, output.Height, syncBlock.Height) { bals.Spendable += output.Amount @@ -2022,12 +2054,17 @@ func (c CreditCategory) String() string { // this package at a later time. func RecvCategory(details *wtxmgr.TxDetails, syncHeight int32, net *chaincfg.Params) CreditCategory { if blockchain.IsCoinBaseTx(&details.MsgTx) { - if confirmed(int32(net.CoinbaseMaturity), details.Block.Height, - syncHeight) { + if confirmed(int32(net.CoinbaseMaturity), details.Block.Height, syncHeight) { return CreditGenerate } return CreditImmature } + if blockchain.IsHogExTx(&details.MsgTx) { + if confirmed(int32(net.MwebPegoutMaturity), details.Block.Height, syncHeight) { + return CreditReceive + } + return CreditImmature + } return CreditReceive } @@ -2655,8 +2692,7 @@ func (w *Wallet) AccountBalances(scope waddrmgr.KeyScope, if !confirmed(requiredConfs, output.Height, syncBlock.Height) { continue } - if output.FromCoinBase && !confirmed(int32(w.ChainParams().CoinbaseMaturity), - output.Height, syncBlock.Height) { + if !output.IsMature(syncBlock.Height, w.chainParams) { continue } _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) @@ -2754,12 +2790,9 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32, continue } - // Only mature coinbase outputs are included. - if output.FromCoinBase { - target := int32(w.ChainParams().CoinbaseMaturity) - if !confirmed(target, output.Height, syncBlock.Height) { - continue - } + // Only mature outputs are included. + if !output.IsMature(syncBlock.Height, w.chainParams) { + continue } // Exclude locked outputs from the result set. @@ -2827,6 +2860,8 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32, return err } spendable = true + case txscript.MwebTy: + spendable = true } result := &btcjson.ListUnspentResult{ @@ -3060,11 +3095,11 @@ func (w *Wallet) ReleaseOutput(id wtxmgr.LockID, op wire.OutPoint) error { // credits that are not known to have been mined into a block, and attempts // to send each to the chain server for relay. func (w *Wallet) resendUnminedTxs() { - var txs []*wire.MsgTx + var txs []*wtxmgr.TxRecord err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) var err error - txs, err = w.TxStore.UnminedTxs(txmgrNs) + txs, err = w.TxStore.UnminedTxRecords(txmgrNs) return err }) if err != nil { @@ -3077,7 +3112,7 @@ func (w *Wallet) resendUnminedTxs() { txHash, err := w.publishTransaction(tx) if err != nil { log.Debugf("Unable to rebroadcast transaction %v: %v", - tx.TxHash(), err) + tx.Hash, err) continue } @@ -3399,7 +3434,8 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, return createdTx.Tx, ErrTxUnsigned } - txHash, err := w.reliablyPublishTransaction(createdTx.Tx, label) + txHash, err := w.reliablyPublishTransaction( + createdTx.Tx, createdTx.NewMwebCoins, label) if err != nil { return nil, err } @@ -3655,7 +3691,7 @@ func (e *ErrInMempool) Unwrap() error { // This function is unstable and will be removed once syncing code is moved out // of the wallet. func (w *Wallet) PublishTransaction(tx *wire.MsgTx, label string) error { - _, err := w.reliablyPublishTransaction(tx, label) + _, err := w.reliablyPublishTransaction(tx, nil, label) return err } @@ -3665,7 +3701,7 @@ func (w *Wallet) PublishTransaction(tx *wire.MsgTx, label string) error { // the database (along with cleaning up all inputs used, and outputs created) if // the transaction is rejected by the backend. func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx, - label string) (*chainhash.Hash, error) { + newMwebCoins []*mweb.Coin, label string) (*chainhash.Hash, error) { chainClient, err := w.requireChainClient() if err != nil { @@ -3686,6 +3722,8 @@ func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx, var ourAddrs []ltcutil.Address err = walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { addrmgrNs := dbTx.ReadWriteBucket(waddrmgrNamespaceKey) + txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) + for _, txOut := range tx.TxOut { _, addrs, _, err := txscript.ExtractPkScriptAddrs( txOut.PkScript, w.chainParams, @@ -3709,10 +3747,23 @@ func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx, } } + for _, coin := range newMwebCoins { + addr := ltcutil.NewAddressMweb(coin.Address, w.chainParams) + for _, kp := range w.mwebKeyPools { + if _, err = kp.contains(addrmgrNs, addr); err != nil { + return err + } + } + err = w.addMwebOutpoint(txmgrNs, txRec, int64(coin.Value), + addr.ScriptAddress(), coin.OutputId) + if err != nil { + return err + } + } + // If there is a label we should write, get the namespace key // and record it in the tx store. if len(label) != 0 { - txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) if err = w.TxStore.PutTxLabel(txmgrNs, tx.TxHash(), label); err != nil { return err } @@ -3731,24 +3782,30 @@ func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx, return nil, err } - return w.publishTransaction(tx) + return w.publishTransaction(txRec) } // publishTransaction attempts to send an unconfirmed transaction to the // wallet's current backend. In the event that sending the transaction fails for // whatever reason, it will be removed from the wallet's unconfirmed transaction // store. -func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { +func (w *Wallet) publishTransaction(txRec *wtxmgr.TxRecord) (*chainhash.Hash, error) { chainClient, err := w.requireChainClient() if err != nil { return nil, err } var ( - txid = tx.TxHash() + tx = &wire.MsgTx{} + txid = txRec.Hash returnErr error ) + err = tx.Deserialize(bytes.NewReader(txRec.BroadcastTx)) + if err != nil { + return nil, err + } + _, err = chainClient.SendRawTransaction(tx, false) if err == nil { return &txid, nil @@ -3767,10 +3824,6 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { case errors.As(returnErr, &errAlreadyConfirmed): dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) - txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) - if err != nil { - return err - } return w.TxStore.RemoveUnminedTx(txmgrNs, txRec) }) if dbErr != nil { @@ -3793,10 +3846,6 @@ func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) { // wallet won't be accurate. dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) - txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now()) - if err != nil { - return err - } return w.TxStore.RemoveUnminedTx(txmgrNs, txRec) }) if dbErr != nil { @@ -4019,6 +4068,7 @@ func OpenWithRetry(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks, chainParams: params, quit: make(chan struct{}), syncRetryInterval: syncRetryInterval, + mwebKeyPools: map[skmAccount]*mwebKeyPool{}, } w.NtfnServer = newNotificationServer(w) diff --git a/walletdb/go.mod b/walletdb/go.mod index d17dadd0f9..6c8c77a367 100644 --- a/walletdb/go.mod +++ b/walletdb/go.mod @@ -5,7 +5,7 @@ go 1.12 require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/davecgh/go-spew v1.1.1 - github.com/stretchr/testify v1.8.2 // indirect + github.com/stretchr/testify v1.8.3 // indirect go.etcd.io/bbolt v1.3.7 - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.13.0 // indirect ) diff --git a/walletdb/go.sum b/walletdb/go.sum index e3faf084a9..9771666a04 100644 --- a/walletdb/go.sum +++ b/walletdb/go.sum @@ -11,14 +11,12 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/wtxmgr/db.go b/wtxmgr/db.go index efea3aa629..8d18ec4f27 100644 --- a/wtxmgr/db.go +++ b/wtxmgr/db.go @@ -70,6 +70,7 @@ var ( bucketUnminedCredits = []byte("mc") bucketUnminedInputs = []byte("mi") bucketLockedOutputs = []byte("lo") + bucketMwebOutpoints = []byte("mo") ) // Root (namespace) bucket keys @@ -368,7 +369,9 @@ func deleteBlockRecord(ns walletdb.ReadWriteBucket, height int32) error { // The record value is serialized as such: // // [0:8] Received time (8 bytes) -// [8:] Serialized transaction (varies) +// [8:n] Serialized transaction (varies) +// [n:n+4] Size of broadcast transaction +// [n+4:m] Broadcast transaction (varies) func keyTxRecord(txHash *chainhash.Hash, block *Block) []byte { k := make([]byte, 68) @@ -379,22 +382,20 @@ func keyTxRecord(txHash *chainhash.Hash, block *Block) []byte { } func valueTxRecord(rec *TxRecord) ([]byte, error) { - var v []byte + var buf bytes.Buffer + binary.Write(&buf, byteOrder, uint64(rec.Received.Unix())) if rec.SerializedTx == nil { - txSize := rec.MsgTx.SerializeSize() - v = make([]byte, 8, 8+txSize) - err := rec.MsgTx.Serialize(bytes.NewBuffer(v[8:])) + err := rec.MsgTx.Serialize(&buf) if err != nil { str := fmt.Sprintf("unable to serialize transaction %v", rec.Hash) return nil, storeError(ErrInput, str, err) } - v = v[:cap(v)] } else { - v = make([]byte, 8+len(rec.SerializedTx)) - copy(v[8:], rec.SerializedTx) + buf.Write(rec.SerializedTx) } - byteOrder.PutUint64(v, uint64(rec.Received.Unix())) - return v, nil + binary.Write(&buf, byteOrder, uint32(len(rec.BroadcastTx))) + buf.Write(rec.BroadcastTx) + return buf.Bytes(), nil } func putTxRecord(ns walletdb.ReadWriteBucket, rec *TxRecord, block *Block) error { @@ -428,12 +429,27 @@ func readRawTxRecord(txHash *chainhash.Hash, v []byte, rec *TxRecord) error { } rec.Hash = *txHash rec.Received = time.Unix(int64(byteOrder.Uint64(v)), 0) - err := rec.MsgTx.Deserialize(bytes.NewReader(v[8:])) + buf := bytes.NewReader(v[8:]) + err := rec.MsgTx.Deserialize(buf) if err != nil { str := fmt.Sprintf("%s: failed to deserialize transaction %v", bucketTxRecords, txHash) return storeError(ErrData, str, err) } + var n uint32 + err = binary.Read(buf, byteOrder, &n) + if err != nil { + str := fmt.Sprintf("%s: failed to read length", bucketTxRecords) + return storeError(ErrData, str, err) + } + if n > 0 { + rec.BroadcastTx = make([]byte, n) + _, err = buf.Read(rec.BroadcastTx) + if err != nil { + str := fmt.Sprintf("%s: failed to read BroadcastTx", bucketTxRecords) + return storeError(ErrData, str, err) + } + } return nil } @@ -991,7 +1007,9 @@ func (it *debitIterator) next() bool { // transaction hash. The value matches that of mined transaction records: // // [0:8] Received time (8 bytes) -// [8:] Serialized transaction (varies) +// [8:n] Serialized transaction (varies) +// [n:n+4] Size of broadcast transaction +// [n+4:m] Broadcast transaction (varies) func putRawUnmined(ns walletdb.ReadWriteBucket, k, v []byte) error { err := ns.NestedReadWriteBucket(bucketUnmined).Put(k, v) @@ -1405,6 +1423,43 @@ func forEachLockedOutput(ns walletdb.ReadBucket, }) } +// The mweb outpoint index records synthetic outpoints for mweb outputs +// and is keyed by mweb output id. +// +// Values use the canonical outpoint serialization: +// +// [0:32] Transaction hash (32 bytes) +// [32:36] Output index (4 bytes) + +func putMwebOutpoint(ns walletdb.ReadWriteBucket, + outputId *chainhash.Hash, outPoint *wire.OutPoint) error { + + v := canonicalOutPoint(&outPoint.Hash, outPoint.Index) + err := ns.NestedReadWriteBucket(bucketMwebOutpoints).Put(outputId[:], v) + if err != nil { + str := "cannot put mweb outpoint" + return storeError(ErrDatabase, str, err) + } + return nil +} + +// existsMwebOutpoint returns the synthetic outpoint for the mweb output. +// If there is no outpoint recorded, the result is nil. +func existsMwebOutpoint(ns walletdb.ReadBucket, + outputId *chainhash.Hash) (*wire.OutPoint, error) { + + v := ns.NestedReadBucket(bucketMwebOutpoints).Get(outputId[:]) + if v == nil { + return nil, nil + } + var op wire.OutPoint + err := readCanonicalOutPoint(v, &op) + if err != nil { + return nil, err + } + return &op, nil +} + // openStore opens an existing transaction store from the passed namespace. func openStore(ns walletdb.ReadBucket) error { version, err := fetchVersion(ns) @@ -1504,6 +1559,10 @@ func createBuckets(ns walletdb.ReadWriteBucket) error { str := "failed to create locked outputs bucket" return storeError(ErrDatabase, str, err) } + if _, err := ns.CreateBucket(bucketMwebOutpoints); err != nil { + str := "failed to create mweb outpoints bucket" + return storeError(ErrDatabase, str, err) + } return nil } @@ -1548,6 +1607,10 @@ func deleteBuckets(ns walletdb.ReadWriteBucket) error { str := "failed to delete locked outputs bucket" return storeError(ErrDatabase, str, err) } + if err := ns.DeleteNestedBucket(bucketMwebOutpoints); err != nil { + str := "failed to delete mweb outpoints bucket" + return storeError(ErrDatabase, str, err) + } return nil } diff --git a/wtxmgr/go.mod b/wtxmgr/go.mod index 428b36c33d..970c5d60a2 100644 --- a/wtxmgr/go.mod +++ b/wtxmgr/go.mod @@ -1,21 +1,5 @@ module github.com/ltcsuite/ltcwallet/wtxmgr -go 1.12 +go 1.18 -require ( - github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/kr/pretty v0.3.0 // indirect - github.com/ltcsuite/lnd/clock v1.1.0 - github.com/ltcsuite/ltcd v0.23.5 - github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 - github.com/ltcsuite/ltcd/ltcutil v1.1.3 - github.com/ltcsuite/ltcwallet/walletdb v1.3.5 - github.com/onsi/gomega v1.26.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect - github.com/stretchr/testify v1.8.2 // indirect - go.etcd.io/bbolt v1.3.7 // indirect - golang.org/x/crypto v0.7.0 // indirect - golang.org/x/net v0.10.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect -) +require github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f diff --git a/wtxmgr/go.sum b/wtxmgr/go.sum index 74ba62fa27..67c8df9153 100644 --- a/wtxmgr/go.sum +++ b/wtxmgr/go.sum @@ -1,259 +1,2 @@ -github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= -github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= -github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ltcsuite/lnd/clock v1.1.0 h1:am2L2QTKgotbUSILSLLdZNe1ziSacXfR34Ry0f8npCs= -github.com/ltcsuite/lnd/clock v1.1.0/go.mod h1:Nn3X2+B0/70MnytJPHvOf3SPTPno09v90J/KJf5/dA8= -github.com/ltcsuite/ltcd v0.23.5 h1:MFWjmx2hCwxrUu9v0wdIPOSN7PHg9BWQeh+AO4FsVLI= -github.com/ltcsuite/ltcd v0.23.5/go.mod h1:JV6swXR5m0cYFi0VYdQPp3UnMdaDQxaRUCaU1PPjb+g= -github.com/ltcsuite/ltcd/btcec/v2 v2.3.2 h1:HVArUNQGqGaSSoyYkk9qGht74U0/uNhS0n7jV9rkmno= -github.com/ltcsuite/ltcd/btcec/v2 v2.3.2/go.mod h1:T1t5TjbjPnryvlGQ+RpSKGuU8KhjNN7rS5+IznPj1VM= -github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2 h1:xuWxvRKxLvOKuS7/Q/7I3tpc3cWAB0+hZpU8YdVqkzg= -github.com/ltcsuite/ltcd/chaincfg/chainhash v1.0.2/go.mod h1:nkLkAFGhursWf2U68gt61hPieK1I+0m78e+2aevNyD8= -github.com/ltcsuite/ltcd/ltcutil v1.1.3 h1:8AapjCPLIt/wtYe6Odfk1EC2y9mcbpgjyxyCoNjAkFI= -github.com/ltcsuite/ltcd/ltcutil v1.1.3/go.mod h1:z8txd/ohBFrOMBUT70K8iZvHJD/Vc3gzx+6BP6cBxQw= -github.com/ltcsuite/ltcwallet/walletdb v1.3.5 h1:WymVw0FBQ8KJgH7B88ujRqBOJ9R0en9K9urpJW4atAE= -github.com/ltcsuite/ltcwallet/walletdb v1.3.5/go.mod h1:29SBzxA55wNxY3ctFw6t5PgsULwf3NMwg2MiGQgtrJE= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= -github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= -github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= -github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= -github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= -github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= -github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= -github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= -github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= -github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= -github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= -github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= -go.etcd.io/gofail v0.1.0/go.mod h1:VZBCXYGZhHAinaBiiqYvuDynvahNsAyLFwB3kEHKz1M= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= -lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/wtxmgr/tx.go b/wtxmgr/tx.go index ff7d6ae4f1..ca4dc0fa10 100644 --- a/wtxmgr/tx.go +++ b/wtxmgr/tx.go @@ -124,6 +124,7 @@ type TxRecord struct { Hash chainhash.Hash Received time.Time SerializedTx []byte // Optional: may be nil + BroadcastTx []byte // Optional: may be nil } // LockedOutput is a type that contains an outpoint of an UTXO and its lock @@ -161,11 +162,16 @@ func NewTxRecordFromMsgTx(msgTx *wire.MsgTx, received time.Time) (*TxRecord, err return nil, storeError(ErrInput, str, err) } rec := &TxRecord{ - MsgTx: *msgTx, Received: received, SerializedTx: buf.Bytes(), + BroadcastTx: buf.Bytes(), Hash: msgTx.TxHash(), } + err = rec.MsgTx.Deserialize(buf) + if err != nil { + str := "failed to deserialize transaction" + return nil, storeError(ErrInput, str, err) + } return rec, nil } @@ -178,8 +184,24 @@ type Credit struct { BlockMeta Amount ltcutil.Amount PkScript []byte + MwebOutput *wire.MwebOutput Received time.Time FromCoinBase bool + IsMwebPegout bool +} + +func (cred *Credit) IsMature(height int32, chainParams *chaincfg.Params) bool { + confirms := height - cred.Height + 1 + if cred.Height < 0 { + confirms = 0 + } + if cred.FromCoinBase && confirms < int32(chainParams.CoinbaseMaturity) { + return false + } + if cred.IsMwebPegout && confirms < int32(chainParams.MwebPegoutMaturity) { + return false + } + return true } // LockID represents a unique context-specific ID assigned to an output lock. @@ -398,6 +420,52 @@ func (s *Store) RemoveUnminedTx(ns walletdb.ReadWriteBucket, rec *TxRecord) erro return s.removeConflict(ns, rec) } +// Store a mapping from an mweb output to its synthetic outpoint. +func (s *Store) AddMwebOutpoint(ns walletdb.ReadWriteBucket, + outputId *chainhash.Hash, outpoint *wire.OutPoint) error { + + return putMwebOutpoint(ns, outputId, outpoint) +} + +// Get the synthetic outpoint for an mweb output if it exists. +func (s *Store) GetMwebOutpoint( + ns walletdb.ReadBucket, outputId *chainhash.Hash) ( + op *wire.OutPoint, rec *TxRecord, err error) { + + op, err = existsMwebOutpoint(ns, outputId) + if err != nil || op == nil { + return + } + _, val := latestTxRecord(ns, &op.Hash) + if val == nil { + val = existsRawUnmined(ns, op.Hash[:]) + } + if val != nil { + rec = &TxRecord{} + err = readRawTxRecord(&op.Hash, val, rec) + } + return +} + +// Find the mweb output within the transaction that corresponds +// to the synthetic outpoint. +func (s *Store) GetMwebOutput(ns walletdb.ReadBucket, + op *wire.OutPoint, rec *TxRecord) (*wire.MwebOutput, error) { + + if rec.MsgTx.Mweb != nil { + for _, output := range rec.MsgTx.Mweb.TxBody.Outputs { + outpoint, err := existsMwebOutpoint(ns, output.Hash()) + if err != nil { + return nil, err + } + if outpoint != nil && *outpoint == *op { + return output, nil + } + } + } + return nil, nil +} + // insertMinedTx inserts a new transaction record for a mined transaction into // the database under the confirmed bucket. It guarantees that, if the // tranasction was previously unconfirmed, then it will take care of cleaning up @@ -602,7 +670,9 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error { // not moved to the unconfirmed store. A coinbase cannot // contain any debits, but all credits should be removed // and the mined balance decremented. - if blockchain.IsCoinBaseTx(&rec.MsgTx) { + if blockchain.IsCoinBaseTx(&rec.MsgTx) || + blockchain.IsHogExTx(&rec.MsgTx) { + op := wire.OutPoint{Hash: rec.Hash} for i, output := range rec.MsgTx.TxOut { k, v := existsCredit(ns, &rec.Hash, @@ -797,6 +867,10 @@ func (s *Store) rollback(ns walletdb.ReadWriteBucket, height int32) error { // UnspentOutputs returns all unspent received transaction outputs. // The order is undefined. func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { + return s.UnspentOutputs2(ns, false) +} + +func (s *Store) UnspentOutputs2(ns walletdb.ReadBucket, all bool) ([]Credit, error) { var unspent []Credit var op wire.OutPoint @@ -813,7 +887,7 @@ func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { return nil } - if existsRawUnminedInput(ns, k) != nil { + if existsRawUnminedInput(ns, k) != nil && !all { // Output is spent by an unmined transaction. // Skip this k/v pair. return nil @@ -847,6 +921,11 @@ func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { PkScript: txOut.PkScript, Received: rec.Received, FromCoinBase: blockchain.IsCoinBaseTx(&rec.MsgTx), + IsMwebPegout: blockchain.IsHogExTx(&rec.MsgTx), + } + cred.MwebOutput, err = s.GetMwebOutput(ns, &op, rec) + if err != nil { + return err } unspent = append(unspent, cred) return nil @@ -870,7 +949,7 @@ func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { return nil } - if existsRawUnminedInput(ns, k) != nil { + if existsRawUnminedInput(ns, k) != nil && !all { // Output is spent by an unmined transaction. // Skip to next unmined credit. return nil @@ -896,6 +975,11 @@ func (s *Store) UnspentOutputs(ns walletdb.ReadBucket) ([]Credit, error) { PkScript: txOut.PkScript, Received: rec.Received, FromCoinBase: blockchain.IsCoinBaseTx(&rec.MsgTx), + IsMwebPegout: blockchain.IsHogExTx(&rec.MsgTx), + } + cred.MwebOutput, err = s.GetMwebOutput(ns, &op, &rec) + if err != nil { + return err } unspent = append(unspent, cred) return nil @@ -1023,8 +1107,10 @@ func (s *Store) Balance(ns walletdb.ReadBucket, minConf int32, syncHeight int32) continue } confs := syncHeight - block.Height + 1 - if confs < minConf || (blockchain.IsCoinBaseTx(&rec.MsgTx) && - confs < coinbaseMaturity) { + if confs < minConf || + blockchain.IsCoinBaseTx(&rec.MsgTx) && confs < coinbaseMaturity || + blockchain.IsHogExTx(&rec.MsgTx) && + confs < int32(s.chainParams.MwebPegoutMaturity) { bal -= amt } } diff --git a/wtxmgr/unconfirmed.go b/wtxmgr/unconfirmed.go index 5aa0797bad..307c479372 100644 --- a/wtxmgr/unconfirmed.go +++ b/wtxmgr/unconfirmed.go @@ -6,6 +6,9 @@ package wtxmgr import ( + "bytes" + "slices" + "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/wire" "github.com/ltcsuite/ltcwallet/walletdb" @@ -176,12 +179,36 @@ func (s *Store) UnminedTxs(ns walletdb.ReadBucket) ([]*wire.MsgTx, error) { txSet := make(map[chainhash.Hash]*wire.MsgTx, len(recSet)) for txHash, txRec := range recSet { - txSet[txHash] = &txRec.MsgTx + var tx wire.MsgTx + err = tx.Deserialize(bytes.NewReader(txRec.BroadcastTx)) + if err == nil { + txSet[txHash] = &tx + } else { + txSet[txHash] = &txRec.MsgTx + } } return DependencySort(txSet), nil } +func (s *Store) UnminedTxRecords(ns walletdb.ReadBucket) ([]*TxRecord, error) { + recSet, err := s.unminedTxRecords(ns) + if err != nil { + return nil, err + } + + recs := make([]*TxRecord, 0, len(recSet)) + for _, rec := range recSet { + recs = append(recs, rec) + } + + slices.SortFunc(recs, func(rec1, rec2 *TxRecord) int { + return rec1.Received.Compare(rec2.Received) + }) + + return recs, nil +} + func (s *Store) unminedTxRecords(ns walletdb.ReadBucket) (map[chainhash.Hash]*TxRecord, error) { unmined := make(map[chainhash.Hash]*TxRecord) err := ns.NestedReadBucket(bucketUnmined).ForEach(func(k, v []byte) error {