Skip to content

Commit f42c977

Browse files
committed
backend/btc/address: be explicit about chainIndex/addressIndex
The relative keypath always has two elements, the first element indicating change and the second indicating the address index. A relative keypath of arbitrary length is harder to handle. E.g. it is not compatible with output descriptors with multipath indices (`xpub/<0;1>/*`), where also two elements are required to derive an address (the multipath index selecting between `<X;Y>` and the address index). The unit tests change because the GetAddress test function used an empty relative path instead of a chainIndex and addressIndex, which now was forced to change with the stricter types. I considered keeping `ChainIndex uint32` instead of `Change bool`, because in multipaths, one could have more than two, e.g. `xpub/<X;Y;Z>/*`, but I prefer the stricter type and it's not likely we will ever need more than two (receive & change).
1 parent 65ab4ef commit f42c977

File tree

9 files changed

+72
-37
lines changed

9 files changed

+72
-37
lines changed

backend/backend_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func MockBtcAccount(t *testing.T, config *accounts.AccountConfig, coin *btc.Coin
151151
for _, signingConfig := range config.Config.SigningConfigurations {
152152
addressChain := addresses.NewAddressChain(
153153
signingConfig,
154-
coin.Net(), 20, 0,
154+
coin.Net(), 20, false,
155155
func(*addresses.AccountAddress) (bool, error) {
156156
return false, nil
157157
},

backend/coins/btc/account.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,9 @@ func (account *Account) Initialize() error {
331331
account.log.Infof("gap limits: receive=%d, change=%d", gapLimits.Receive, gapLimits.Change)
332332

333333
subacc.receiveAddresses = addresses.NewAddressChain(
334-
signingConfiguration, account.coin.Net(), int(gapLimits.Receive), 0, account.isAddressUsed, account.log)
334+
signingConfiguration, account.coin.Net(), int(gapLimits.Receive), false, account.isAddressUsed, account.log)
335335
subacc.changeAddresses = addresses.NewAddressChain(
336-
signingConfiguration, account.coin.Net(), int(gapLimits.Change), 1, account.isAddressUsed, account.log)
336+
signingConfiguration, account.coin.Net(), int(gapLimits.Change), true, account.isAddressUsed, account.log)
337337

338338
account.subaccounts = append(account.subaccounts, subacc)
339339
}

backend/coins/btc/addresses/address.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,28 @@ type AccountAddress struct {
4747
}
4848

4949
// NewAccountAddress creates a new account address.
50+
// The chainIndex is 0 for receive and 1 for change.
5051
func NewAccountAddress(
5152
accountConfiguration *signing.Configuration,
52-
keyPath signing.RelativeKeypath,
53+
derivation types.Derivation,
5354
net *chaincfg.Params,
5455
log *logrus.Entry,
5556
) *AccountAddress {
5657

5758
var address btcutil.Address
5859
var redeemScript []byte
59-
configuration, err := accountConfiguration.Derive(keyPath)
60+
configuration, err := accountConfiguration.Derive(
61+
signing.NewEmptyRelativeKeypath().
62+
Child(derivation.SimpleChainIndex(), signing.NonHardened).
63+
Child(derivation.AddressIndex, signing.NonHardened),
64+
)
6065
if err != nil {
6166
log.WithError(err).Panic("Failed to derive the configuration.")
6267
}
6368
log = log.WithFields(logrus.Fields{
64-
"key-path": configuration.AbsoluteKeypath().Encode(),
65-
"configuration": configuration.String(),
69+
"accountConfiguration": accountConfiguration.String(),
70+
"change": derivation.Change,
71+
"addressIndex": derivation.AddressIndex,
6672
})
6773
log.Debug("Creating new account address")
6874

backend/coins/btc/addresses/address_test.go

+19-14
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/addresses"
2323
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/addresses/test"
2424
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/blockchain"
25+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/types"
2526
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/signing"
2627
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
2728
testlog "github.com/BitBoxSwiss/bitbox-wallet-app/util/test"
@@ -38,8 +39,6 @@ func TestMain(m *testing.M) {
3839

3940
var net = &chaincfg.TestNet3Params
4041

41-
var absoluteKeypath = signing.NewEmptyAbsoluteKeypath().Child(0, false).Child(10, false)
42-
4342
type addressTestSuite struct {
4443
suite.Suite
4544

@@ -53,21 +52,24 @@ func TestAddressTestSuite(t *testing.T) {
5352
}
5453

5554
func (s *addressTestSuite) TestNewAddress() {
56-
s.Require().Equal(absoluteKeypath, s.address.Configuration.AbsoluteKeypath())
57-
s.Require().Equal("n2gAErwJCuPmnQuhzPkkWi2haGz9oQxjnX", s.address.EncodeAddress())
55+
expectedKeypath := signing.NewEmptyAbsoluteKeypath().
56+
Child(0, false).
57+
Child(10, false).
58+
Child(0, false).
59+
Child(0, false)
60+
s.Require().Equal(expectedKeypath, s.address.Configuration.AbsoluteKeypath())
61+
s.Require().Equal("moTM88EgqzATgCjSrcNfahXaT9uCy3FHh3", s.address.EncodeAddress())
5862
s.Require().True(s.address.IsForNet(net))
5963
}
6064

6165
func (s *addressTestSuite) TestPubkeyScript() {
62-
payToAddrScript := []byte{
63-
0x76, 0xa9, 0x14, 0xe8, 0x18, 0x5b, 0x34, 0x52, 0x22, 0xbe, 0x2b, 0x77, 0x2f,
64-
0x7a, 0xef, 0x16, 0x2c, 0x11, 0x85, 0x73, 0x2, 0x9d, 0xf4, 0x88, 0xac}
66+
payToAddrScript := []byte{0x76, 0xa9, 0x14, 0x57, 0x12, 0x66, 0x22, 0x63, 0x8b, 0x8d, 0xb3, 0x25, 0xa6, 0x1, 0x35, 0xe2, 0xfd, 0x88, 0x53, 0x74, 0x91, 0xa1, 0xe0, 0x88, 0xac}
6567
s.Require().Equal(payToAddrScript, s.address.PubkeyScript())
6668
}
6769

6870
func (s *addressTestSuite) TestScriptHashHex() {
6971
s.Require().Equal(
70-
blockchain.ScriptHashHex("0466d0029406f583feadaccb91c7b5b855eb5d6782316cafa4f390b7c784436b"),
72+
blockchain.ScriptHashHex("1f4444773ff74188b4d8ccff2a2efec0cae61efce152cafef97b6fadb96382b5"),
7173
s.address.PubkeyScriptHashHex())
7274
}
7375

@@ -79,27 +81,30 @@ func TestAddressP2TR(t *testing.T) {
7981
keypath, err := signing.NewAbsoluteKeypath("m/86'/0'/0'")
8082
require.NoError(t, err)
8183
for _, test := range []struct {
82-
path string
84+
change bool
85+
addressIndex uint32
8386
expectedAddress string
8487
expectedPkScript string
8588
}{
8689
{
87-
path: "0/0",
90+
change: false,
91+
addressIndex: 0,
8892
expectedAddress: "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
8993
expectedPkScript: "5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c",
9094
},
9195
{
92-
path: "0/1",
96+
change: false,
97+
addressIndex: 1,
9398
expectedAddress: "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
9499
expectedPkScript: "5120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb",
95100
},
96101
{
97-
path: "1/0",
102+
change: true,
103+
addressIndex: 0,
98104
expectedAddress: "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
99105
expectedPkScript: "5120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc",
100106
},
101107
} {
102-
relKeypath, err := signing.NewRelativeKeypath(test.path)
103108
require.NoError(t, err)
104109
addr := addresses.NewAccountAddress(
105110
signing.NewBitcoinConfiguration(
@@ -108,7 +113,7 @@ func TestAddressP2TR(t *testing.T) {
108113
keypath,
109114
extendedPublicKey,
110115
),
111-
relKeypath,
116+
types.Derivation{Change: test.change, AddressIndex: test.addressIndex},
112117
&chaincfg.MainNetParams,
113118
logging.Get().WithGroup("addresses_test"),
114119
)

backend/coins/btc/addresses/addresschain.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package addresses
1616

1717
import (
1818
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/blockchain"
19+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/types"
1920
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/signing"
2021
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
2122
"github.com/BitBoxSwiss/bitbox-wallet-app/util/locker"
@@ -28,7 +29,7 @@ type AddressChain struct {
2829
accountConfiguration *signing.Configuration
2930
net *chaincfg.Params
3031
gapLimit int
31-
chainIndex uint32
32+
change bool
3233
addresses []*AccountAddress
3334
addressesLookup map[blockchain.ScriptHashHex]*AccountAddress
3435
addressesLock locker.Locker
@@ -41,20 +42,20 @@ func NewAddressChain(
4142
accountConfiguration *signing.Configuration,
4243
net *chaincfg.Params,
4344
gapLimit int,
44-
chainIndex uint32,
45+
change bool,
4546
isAddressUsed func(*AccountAddress) (bool, error),
4647
log *logrus.Entry,
4748
) *AddressChain {
4849
return &AddressChain{
4950
accountConfiguration: accountConfiguration,
5051
net: net,
5152
gapLimit: gapLimit,
52-
chainIndex: chainIndex,
53+
change: change,
5354
addresses: []*AccountAddress{},
5455
addressesLookup: map[blockchain.ScriptHashHex]*AccountAddress{},
5556
isAddressUsed: isAddressUsed,
5657
log: log.WithFields(logrus.Fields{"group": "addresses", "net": net.Name,
57-
"gap-limit": gapLimit, "chain-index": chainIndex,
58+
"gap-limit": gapLimit, "change": change,
5859
"configuration": accountConfiguration.String()}),
5960
}
6061
}
@@ -79,7 +80,7 @@ func (addresses *AddressChain) addAddress() *AccountAddress {
7980
index := uint32(len(addresses.addresses))
8081
address := NewAccountAddress(
8182
addresses.accountConfiguration,
82-
signing.NewEmptyRelativeKeypath().Child(addresses.chainIndex, signing.NonHardened).Child(index, signing.NonHardened),
83+
types.Derivation{Change: addresses.change, AddressIndex: index},
8384
addresses.net,
8485
addresses.log,
8586
)

backend/coins/btc/addresses/addresschain_test.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type addressChainTestSuite struct {
3232
addresses *addresses.AddressChain
3333
xpub *hdkeychain.ExtendedKey
3434
gapLimit int
35-
chainIndex uint32
35+
change bool
3636
isAddressUsed func(*addresses.AccountAddress) bool
3737
log *logrus.Entry
3838
}
@@ -46,14 +46,14 @@ func (s *addressChainTestSuite) SetupTest() {
4646
panic(err)
4747
}
4848
s.gapLimit = 6
49-
s.chainIndex = 1
49+
s.change = true
5050
s.xpub = xpub
5151
s.addresses = addresses.NewAddressChain(
5252
signing.NewBitcoinConfiguration(
5353
signing.ScriptTypeP2PKH, []byte{1, 2, 3, 4}, signing.NewEmptyAbsoluteKeypath(), xpub),
5454
net,
5555
s.gapLimit,
56-
s.chainIndex,
56+
s.change,
5757
func(address *addresses.AccountAddress) (bool, error) {
5858
return s.isAddressUsed(address), nil
5959
},
@@ -122,7 +122,11 @@ func (s *addressChainTestSuite) TestEnsureAddresses() {
122122
s.Require().Len(newAddresses, s.gapLimit)
123123
// Check that the pubkeys behind the new addresses are derived in sequence from the root xpub.
124124
getPubKey := func(index int) *btcec.PublicKey {
125-
chain, err := s.xpub.Derive(s.chainIndex)
125+
chainIndex := uint32(0)
126+
if s.change {
127+
chainIndex = 1
128+
}
129+
chain, err := s.xpub.Derive(chainIndex)
126130
if err != nil {
127131
panic(err)
128132
}

backend/coins/btc/addresses/test/test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package test
1616

1717
import (
1818
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/addresses"
19+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/types"
1920
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/signing"
2021
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
2122
"github.com/btcsuite/btcd/btcutil/hdkeychain"
@@ -48,7 +49,7 @@ func NewAddressChain(
4849
}
4950
configuration := signing.NewBitcoinConfiguration(
5051
signing.ScriptTypeP2PKH, []byte{1, 2, 3, 4}, derivationPath, xpub)
51-
return configuration, addresses.NewAddressChain(configuration, net, 20, 0, isAddressUsed, log)
52+
return configuration, addresses.NewAddressChain(configuration, net, 20, false, isAddressUsed, log)
5253
}
5354

5455
// GetAddress returns a dummy address for a given address type.
@@ -61,7 +62,7 @@ func GetAddress(scriptType signing.ScriptType) *addresses.AccountAddress {
6162
scriptType, []byte{1, 2, 3, 4}, absoluteKeypath, extendedPublicKey)
6263
return addresses.NewAccountAddress(
6364
configuration,
64-
signing.NewEmptyRelativeKeypath(),
65+
types.Derivation{Change: false, AddressIndex: 0},
6566
net,
6667
logging.Get().WithGroup("addresses_test"),
6768
)

backend/coins/btc/types/types.go

+18
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,21 @@ func (s *Signature) SerializeCompact() []byte {
5252
s.S.FillBytes(result[32:])
5353
return result
5454
}
55+
56+
// Derivation contains the derivation information to derive address-level information from a BTC
57+
// account descriptor.
58+
type Derivation struct {
59+
// Change is true to derive change address details and false to derive receive address details.
60+
Change bool
61+
AddressIndex uint32
62+
}
63+
64+
// SimpleChainIndex returns 0 for receive and 1 or change. This applies to standard BIP-44
65+
// derivations, but not to multipath descriptors.
66+
func (d Derivation) SimpleChainIndex() uint32 {
67+
if d.Change {
68+
return 1
69+
}
70+
71+
return 0
72+
}

backend/devices/bitbox02/keystore_simulator_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,10 @@ func makeTx(t *testing.T, device *Device, recipient *maketx.OutputInfo) *btc.Pro
335335
makeConfig(t, device, signing.ScriptTypeP2WPKH, mustKeypath("m/84'/0'/0'")),
336336
makeConfig(t, device, signing.ScriptTypeP2WPKHP2SH, mustKeypath("m/49'/0'/0'")),
337337
}
338-
inputAddress0 := addresses.NewAccountAddress(configurations[0], signing.NewEmptyRelativeKeypath().Child(0, false).Child(0, false), network, log)
339-
inputAddress1 := addresses.NewAccountAddress(configurations[1], signing.NewEmptyRelativeKeypath().Child(0, false).Child(0, false), network, log)
340-
inputAddress2 := addresses.NewAccountAddress(configurations[2], signing.NewEmptyRelativeKeypath().Child(0, false).Child(0, false), network, log)
341-
changeAddress := addresses.NewAccountAddress(configurations[0], signing.NewEmptyRelativeKeypath().Child(1, false).Child(0, false), network, log)
338+
inputAddress0 := addresses.NewAccountAddress(configurations[0], types.Derivation{Change: false, AddressIndex: 0}, network, log)
339+
inputAddress1 := addresses.NewAccountAddress(configurations[1], types.Derivation{Change: false, AddressIndex: 0}, network, log)
340+
inputAddress2 := addresses.NewAccountAddress(configurations[2], types.Derivation{Change: false, AddressIndex: 0}, network, log)
341+
changeAddress := addresses.NewAccountAddress(configurations[0], types.Derivation{Change: true, AddressIndex: 1}, network, log)
342342

343343
prevTx := &wire.MsgTx{
344344
Version: 2,
@@ -449,7 +449,7 @@ func TestSimulatorSignBTCTransactionSendSelfSameAccount(t *testing.T) {
449449
cfg := makeConfig(t, device, signing.ScriptTypeP2TR, mustKeypath("m/86'/0'/0'"))
450450
selfAddress := addresses.NewAccountAddress(
451451
cfg,
452-
signing.NewEmptyRelativeKeypath().Child(0, false).Child(0, false),
452+
types.Derivation{Change: false, AddressIndex: 0},
453453
network,
454454
log)
455455
proposedTransaction := makeTx(t, device, maketx.NewOutputInfo(selfAddress.PubkeyScript()))

0 commit comments

Comments
 (0)