Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet: Return an error if select utxos in txtoOutputs contain duplicates. #928

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ require (
github.com/jrick/logrotate v1.0.0
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf
github.com/lightninglabs/neutrino v0.16.0
github.com/lightninglabs/neutrino/cache v1.1.1
github.com/lightninglabs/neutrino/cache v1.1.2
github.com/lightningnetwork/lnd/fn v1.0.6
github.com/lightningnetwork/lnd/ticker v1.0.0
github.com/lightningnetwork/lnd/tlv v1.0.2
github.com/stretchr/testify v1.8.4
Expand All @@ -43,6 +44,7 @@ require (
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
github.com/lightninglabs/neutrino v0.16.0 h1:YNTQG32fPR/Zg0vvJVI65OBH8l3U18LSXXtX91hx0q0=
github.com/lightninglabs/neutrino v0.16.0/go.mod h1:x3OmY2wsA18+Kc3TSV2QpSUewOCiscw2mKpXgZv2kZk=
github.com/lightninglabs/neutrino/cache v1.1.1 h1:TllWOSlkABhpgbWJfzsrdUaDH2fBy/54VSIB4vVqV8M=
github.com/lightninglabs/neutrino/cache v1.1.1/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo=
github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g=
github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo=
github.com/lightningnetwork/lnd/clock v1.0.1 h1:QQod8+m3KgqHdvVMV+2DRNNZS1GRFir8mHZYA+Z2hFo=
github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg=
github.com/lightningnetwork/lnd/fn v1.0.6 h1:LOeKEPUMxXcu3wax6zxz7GzNFxSLGMUM6hbHLkujd1s=
github.com/lightningnetwork/lnd/fn v1.0.6/go.mod h1:P027+0CyELd92H9gnReUkGGAqbFA1HwjHWdfaDFD51U=
github.com/lightningnetwork/lnd/queue v1.0.1 h1:jzJKcTy3Nj5lQrooJ3aaw9Lau3I0IwvQR5sqtjdv2R0=
github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms=
github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU=
Expand Down Expand Up @@ -137,6 +139,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
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=
Expand Down
6 changes: 6 additions & 0 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/btcsuite/btcwallet/wallet/txsizes"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/fn"
)

func makeInputSource(eligible []Coin) txauthor.InputSource {
Expand Down Expand Up @@ -179,6 +180,11 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,

var inputSource txauthor.InputSource
if len(selectedUtxos) > 0 {
dedupUtxos := fn.NewSet(selectedUtxos...)
if len(dedupUtxos) != len(selectedUtxos) {
return errors.New("selected UTXOs contain " +
"duplicate values")
}
eligibleByOutpoint := make(
map[wire.OutPoint]wtxmgr.Credit,
)
Expand Down
124 changes: 96 additions & 28 deletions wallet/createtx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,42 +449,110 @@ func TestSelectUtxosTxoToOutpoint(t *testing.T) {
require.Len(t, unspent, 4, "expected 4 unspent "+
"utxos")

selectUtxos := []wire.OutPoint{
tCases := []struct {
name string
selectUtxos []wire.OutPoint
errString string
}{
{
Hash: incomingTx.TxHash(),
Index: 1,
name: "Duplicate utxo values",
selectUtxos: []wire.OutPoint{
{
Hash: incomingTx.TxHash(),
Index: 1,
},
{
Hash: incomingTx.TxHash(),
Index: 1,
},
},
errString: "selected UTXOs contain duplicate values",
},
{
Hash: incomingTx.TxHash(),
Index: 2,
name: "all selected utxos not eligible for spending",
selectUtxos: []wire.OutPoint{
{
Hash: chainhash.Hash([32]byte{1}),
Index: 1,
},
{
Hash: chainhash.Hash([32]byte{3}),
Index: 1,
},
},
errString: "selected outpoint not eligible for " +
"spending",
},
{
name: "some select utxos not eligible for spending",
selectUtxos: []wire.OutPoint{
{
Hash: chainhash.Hash([32]byte{1}),
Index: 1,
},
{
Hash: incomingTx.TxHash(),
Index: 1,
},
},
errString: "selected outpoint not eligible for " +
"spending",
},
{
name: "select utxo, no duplicates and all eligible " +
"for spending",
selectUtxos: []wire.OutPoint{
{
Hash: incomingTx.TxHash(),
Index: 1,
},
{
Hash: incomingTx.TxHash(),
Index: 2,
},
},
},
}

// Test by sending 200_000.
targetTxOut := &wire.TxOut{
Value: 200_000,
PkScript: p2trScript,
}
tx1, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, nil, nil, 0, 1, 1000,
CoinSelectionLargest, true, selectUtxos, alwaysAllowUtxo,
)
require.NoError(t, err)
for _, tc := range tCases {
t.Run(tc.name, func(t *testing.T) {
// Test by sending 200_000.
targetTxOut := &wire.TxOut{
Value: 200_000,
PkScript: p2trScript,
}
tx1, err := w.txToOutputs(
[]*wire.TxOut{targetTxOut}, nil, nil, 0, 1,
1000, CoinSelectionLargest, true,
tc.selectUtxos, alwaysAllowUtxo,
)
if tc.errString != "" {
require.ErrorContains(t, err, tc.errString)
require.Nil(t, tx1)

// We expect all and only our select utxos to be input in this
// transaction.
require.Len(t, tx1.Tx.TxIn, len(selectUtxos))
return
}

lookupSelectUtxos := make(map[wire.OutPoint]struct{})
for _, utxo := range selectUtxos {
lookupSelectUtxos[utxo] = struct{}{}
}
require.NoError(t, err)
require.NotNil(t, tx1)

for _, tx := range tx1.Tx.TxIn {
_, ok := lookupSelectUtxos[tx.PreviousOutPoint]
require.True(t, ok, "unexpected outpoint in txin")
}
// We expect all and only our select utxos to be input
// in this transaction.
require.Len(t, tx1.Tx.TxIn, len(tc.selectUtxos))

// Expect two outputs, change and the actual payment to the address.
require.Len(t, tx1.Tx.TxOut, 2)
lookupSelectUtxos := make(map[wire.OutPoint]struct{})
for _, utxo := range tc.selectUtxos {
lookupSelectUtxos[utxo] = struct{}{}
}

for _, tx := range tx1.Tx.TxIn {
_, ok := lookupSelectUtxos[tx.PreviousOutPoint]
require.True(t, ok)
}

// Expect two outputs, change and the actual payment to
// the address.
require.Len(t, tx1.Tx.TxOut, 2)
})
}
}
61 changes: 55 additions & 6 deletions wallet/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ func (w *Wallet) ImportAccountDryRun(name string,
// intend to not support importing BIP-44 keys into the wallet using the legacy
// pay-to-pubkey-hash (P2PKH) scheme.
func (w *Wallet) ImportPublicKey(pubKey *btcec.PublicKey,
addrType waddrmgr.AddressType) error {
addrType waddrmgr.AddressType, bs *waddrmgr.BlockStamp, rescan bool) error {

// Determine what key scope the public key should belong to and import
// it into the key scope's default imported account.
Expand Down Expand Up @@ -410,18 +410,67 @@ func (w *Wallet) ImportPublicKey(pubKey *btcec.PublicKey,
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
addr, err = scopedKeyManager.ImportPublicKey(ns, pubKey, nil)
return err
if err != nil {
return err
}

// We'll only update our birthday with the new one if it is
// before our current one. Otherwise, if we do, we can
// potentially miss detecting relevant chain events that
// occurred between them while rescanning.
birthdayBlock, _, err := w.Manager.BirthdayBlock(ns)
if err != nil {
return err
}
if bs.Height >= birthdayBlock.Height {
return nil
}

err = w.Manager.SetBirthday(ns, bs.Timestamp)
if err != nil {
return err
}

// To ensure this birthday block is correct, we'll mark it as
// unverified to prompt a sanity check at the next restart to
// ensure it is correct as it was provided by the caller.
return w.Manager.SetBirthdayBlock(ns, *bs, false)
})
if err != nil {
return err
}

log.Infof("Imported address %v", addr.Address())

err = w.chainClient.NotifyReceived([]btcutil.Address{addr.Address()})
if err != nil {
return fmt.Errorf("unable to subscribe for address "+
"notifications: %w", err)
// Rescan blockchain for transactions with txout scripts paying to the
// imported address.
if rescan {
log.Infof("Rescanning oooooo 09")
job := &RescanJob{
Addrs: []btcutil.Address{addr.Address()},
OutPoints: nil,
BlockStamp: *bs,
}

// Submit rescan job and log when the import has completed.
// Do not block on finishing the rescan. The rescan success
// or failure is logged elsewhere, and the channel is not
// required to be read, so discard the return value.
errChan := w.SubmitRescan(job)

err = <-errChan
if err != nil {
return err
}

} else {
err = w.chainClient.NotifyReceived(
[]btcutil.Address{addr.Address()},
)
if err != nil {
return fmt.Errorf("unable to subscribe for address "+
"notifications: %w", err)
}
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion wallet/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ func testImportAccount(t *testing.T, w *Wallet, tc *testCase, watchOnly bool,
require.NoError(t, err)
require.Equal(t, tc.expectedScope, acct2.KeyScope)

err = w.ImportPublicKey(acct3ExternalPub, tc.addrType)
err = w.ImportPublicKey(acct3ExternalPub, tc.addrType, nil, false)
require.NoError(t, err)

// If the wallet is watch only, there is no default account and our
Expand Down
Loading