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: Avoid duplicate address creation #941

Merged
merged 2 commits into from
Jul 18, 2024
Merged
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: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,23 @@ require (
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.7.0
golang.org/x/net v0.10.0
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
golang.org/x/term v0.8.0
google.golang.org/grpc v1.53.0
)

require (
github.com/aead/siphash v1.0.1 // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/lru v1.0.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/kkdai/bstream v1.0.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/lightningnetwork/lnd/clock v1.0.1 // indirect
github.com/lightningnetwork/lnd/queue v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
Expand Down
4 changes: 1 addition & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13P
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.22.0-beta.0.20220207191057-4dc4ff7963b4/go.mod h1:7alexyj/lHlOtr2PJK7L/+HDJZpcGDn/pAU98r7DY08=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
github.com/btcsuite/btcd v0.24.1-0.20240318151728-2fc99e0496d2 h1:b7EiiYEZypI2id3516ptqjzhUfFAgNfF4YVtxikAg6Y=
github.com/btcsuite/btcd v0.24.1-0.20240318151728-2fc99e0496d2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240625142744-cc26860b4026 h1:s8/96vQSj05bqLl9RyM/eMX8gLtiayEj520TVE4YGy0=
github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240625142744-cc26860b4026/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
Expand Down Expand Up @@ -43,7 +41,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
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 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -147,6 +144,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
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 h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/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=
Expand Down
12 changes: 12 additions & 0 deletions wallet/createtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,18 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut,
strategy = CoinSelectionLargest
}

// The addrMgrWithChangeSource function of the wallet creates a
// new change address. The address manager uses OnCommit on the
// walletdb tx to update the in-memory state of the account
// state. But because the commit happens _after_ the account
// manager internal lock has been released, there is a chance
// for the address index to be accessed concurrently, even
// though the closure in OnCommit re-acquires the lock. To avoid
// this issue, we surround the whole address creation process
// with a lock.
w.newAddrMtx.Lock()
defer w.newAddrMtx.Unlock()

var tx *txauthor.AuthoredTx
err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error {
addrmgrNs, changeSource, err := w.addrMgrWithChangeSource(
Expand Down
9 changes: 9 additions & 0 deletions wallet/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,15 @@ func (w *Wallet) ImportAccountDryRun(name string,
*waddrmgr.AccountProperties, []waddrmgr.ManagedAddress,
[]waddrmgr.ManagedAddress, error) {

// The address manager uses OnCommit on the walletdb tx to update the
// in-memory state of the account state. But because the commit happens
// _after_ the account manager internal lock has been released, there
// is a chance for the address index to be accessed concurrently, even
// though the closure in OnCommit re-acquires the lock. To avoid this
// issue, we surround the whole address creation process with a lock.
guggero marked this conversation as resolved.
Show resolved Hide resolved
w.newAddrMtx.Lock()
defer w.newAddrMtx.Unlock()

var (
accountProps *waddrmgr.AccountProperties
externalAddrs []waddrmgr.ManagedAddress
Expand Down
13 changes: 13 additions & 0 deletions wallet/psbt.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,
opts.changeKeyScope = keyScope
}

// The addrMgrWithChangeSource function of the wallet creates a
// new change address. The address manager uses OnCommit on the
// walletdb tx to update the in-memory state of the account
// state. But because the commit happens _after_ the account
// manager internal lock has been released, there is a chance
// for the address index to be accessed concurrently, even
// though the closure in OnCommit re-acquires the lock. To avoid
// this issue, we surround the whole address creation process
// with a lock.
w.newAddrMtx.Lock()

// We also need a change source which needs to be able to insert
// a new change address into the database.
err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error {
Expand All @@ -190,6 +201,8 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,

return nil
})
w.newAddrMtx.Unlock()

if err != nil {
return 0, fmt.Errorf("could not add change address to "+
"database: %w", err)
Expand Down
29 changes: 29 additions & 0 deletions wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ type Wallet struct {
chainClientSynced bool
chainClientSyncMtx sync.Mutex

newAddrMtx sync.Mutex

lockedOutpoints map[wire.OutPoint]struct{}
lockedOutpointsMtx sync.Mutex

Expand Down Expand Up @@ -1693,6 +1695,15 @@ func (w *Wallet) CurrentAddress(account uint32, scope waddrmgr.KeyScope) (btcuti
return nil, err
}

// The address manager uses OnCommit on the walletdb tx to update the
// in-memory state of the account state. But because the commit happens
// _after_ the account manager internal lock has been released, there
// is a chance for the address index to be accessed concurrently, even
// though the closure in OnCommit re-acquires the lock. To avoid this
// issue, we surround the whole address creation process with a lock.
w.newAddrMtx.Lock()
defer w.newAddrMtx.Unlock()

var (
addr btcutil.Address
props *waddrmgr.AccountProperties
Expand Down Expand Up @@ -3153,6 +3164,15 @@ func (w *Wallet) NewAddress(account uint32,
return nil, err
}

// The address manager uses OnCommit on the walletdb tx to update the
// in-memory state of the account state. But because the commit happens
// _after_ the account manager internal lock has been released, there
// is a chance for the address index to be accessed concurrently, even
// though the closure in OnCommit re-acquires the lock. To avoid this
// issue, we surround the whole address creation process with a lock.
w.newAddrMtx.Lock()
defer w.newAddrMtx.Unlock()

var (
addr btcutil.Address
props *waddrmgr.AccountProperties
Expand Down Expand Up @@ -3211,6 +3231,15 @@ func (w *Wallet) NewChangeAddress(account uint32,
return nil, err
}

// The address manager uses OnCommit on the walletdb tx to update the
// in-memory state of the account state. But because the commit happens
// _after_ the account manager internal lock has been released, there
// is a chance for the address index to be accessed concurrently, even
// though the closure in OnCommit re-acquires the lock. To avoid this
// issue, we surround the whole address creation process with a lock.
w.newAddrMtx.Lock()
defer w.newAddrMtx.Unlock()

var addr btcutil.Address
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
Expand Down
60 changes: 57 additions & 3 deletions wallet/wallet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package wallet

import (
"encoding/hex"
"fmt"
"sync"
"testing"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
"github.com/stretchr/testify/require"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"golang.org/x/sync/errgroup"
)

var (
Expand Down Expand Up @@ -305,3 +308,54 @@ func TestGetTransaction(t *testing.T) {
})
}
}

// TestDuplicateAddressDerivation tests that duplicate addresses are not
// derived when multiple goroutines are concurrently requesting new addresses.
func TestDuplicateAddressDerivation(t *testing.T) {
w, cleanup := testWallet(t)
defer cleanup()

var (
m sync.Mutex
globalAddrs = make(map[string]btcutil.Address)
)

for o := 0; o < 10; o++ {
var eg errgroup.Group

for n := 0; n < 10; n++ {
eg.Go(func() error {
addrs := make([]btcutil.Address, 10)
for i := 0; i < 10; i++ {
addr, err := w.NewAddress(
0, waddrmgr.KeyScopeBIP0084,
)
if err != nil {
return err
}

addrs[i] = addr
}

m.Lock()
defer m.Unlock()

for idx := range addrs {
addrStr := addrs[idx].String()
if a, ok := globalAddrs[addrStr]; ok {
return fmt.Errorf("duplicate "+
"address! already "+
"have %v, want to "+
"add %v", a, addrs[idx])
}

globalAddrs[addrStr] = addrs[idx]
}

return nil
})
}

require.NoError(t, eg.Wait())
}
}
Loading