Skip to content

Commit 58e37ff

Browse files
committed
backend/btc: remove address-level signing config
The previous commits moved towards using an account config plus derivation info to get to the address. This commit removes the address-level signing config for BTC. This is a simplification, as it does not make sense to derive a full "signing config" when all that is derived is the keypath, nothing else changes. This is part of moving to general descriptor support. There are still remaining issues where code assumes it's a single-sig (calls to address.AbsoluteKeypath(), and NewAddress(), etc.), but we can solve that later step by step.
1 parent 8625187 commit 58e37ff

File tree

12 files changed

+72
-66
lines changed

12 files changed

+72
-66
lines changed

backend/coins/btc/addresses/address.go

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
ourbtcutil "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/util"
2323
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/signing"
2424
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
25+
"github.com/btcsuite/btcd/btcec/v2"
2526
"github.com/btcsuite/btcd/btcec/v2/schnorr"
2627
"github.com/btcsuite/btcd/btcutil"
2728
"github.com/btcsuite/btcd/chaincfg"
@@ -37,9 +38,9 @@ type AccountAddress struct {
3738

3839
// AccountConfiguration is the account level configuration from which this address was derived.
3940
AccountConfiguration *signing.Configuration
40-
// Configuration contains the absolute keypath and the extended public keys of the address.
41-
Configuration *signing.Configuration
42-
Derivation types.Derivation
41+
// publicKey is the public key of a single-sig address.
42+
publicKey *btcec.PublicKey
43+
Derivation types.Derivation
4344

4445
// redeemScript stores the redeem script of a BIP16 P2SH output or nil if address type is P2PKH.
4546
redeemScript []byte
@@ -55,25 +56,29 @@ func NewAccountAddress(
5556
log *logrus.Entry,
5657
) *AccountAddress {
5758

58-
var address btcutil.Address
59-
var redeemScript []byte
60-
configuration, err := accountConfiguration.Derive(
61-
signing.NewEmptyRelativeKeypath().
62-
Child(derivation.SimpleChainIndex(), signing.NonHardened).
63-
Child(derivation.AddressIndex, signing.NonHardened),
64-
)
65-
if err != nil {
66-
log.WithError(err).Panic("Failed to derive the configuration.")
67-
}
6859
log = log.WithFields(logrus.Fields{
6960
"accountConfiguration": accountConfiguration.String(),
7061
"change": derivation.Change,
7162
"addressIndex": derivation.AddressIndex,
7263
})
7364
log.Debug("Creating new account address")
7465

75-
publicKeyHash := btcutil.Hash160(configuration.PublicKey().SerializeCompressed())
76-
switch configuration.ScriptType() {
66+
var address btcutil.Address
67+
var redeemScript []byte
68+
relativeKeypath := signing.NewEmptyRelativeKeypath().
69+
Child(derivation.SimpleChainIndex(), signing.NonHardened).
70+
Child(derivation.AddressIndex, signing.NonHardened)
71+
derivedXpub, err := relativeKeypath.Derive(accountConfiguration.ExtendedPublicKey())
72+
if err != nil {
73+
log.WithError(err).Panic("Failed to derive xpub.")
74+
}
75+
publicKey, err := derivedXpub.ECPubKey()
76+
if err != nil {
77+
log.WithError(err).Panic("Failed to convert an extended public key to a normal public key.")
78+
}
79+
80+
publicKeyHash := btcutil.Hash160(publicKey.SerializeCompressed())
81+
switch accountConfiguration.ScriptType() {
7782
case signing.ScriptTypeP2PKH:
7883
address, err = btcutil.NewAddressPubKeyHash(publicKeyHash, net)
7984
if err != nil {
@@ -99,19 +104,19 @@ func NewAccountAddress(
99104
log.WithError(err).Panic("Failed to get p2wpkh addr. from publ. key hash.")
100105
}
101106
case signing.ScriptTypeP2TR:
102-
outputKey := txscript.ComputeTaprootKeyNoScript(configuration.PublicKey())
107+
outputKey := txscript.ComputeTaprootKeyNoScript(publicKey)
103108
address, err = btcutil.NewAddressTaproot(schnorr.SerializePubKey(outputKey), net)
104109
if err != nil {
105110
log.WithError(err).Panic("Failed to get p2tr addr")
106111
}
107112
default:
108-
log.Panic(fmt.Sprintf("Unrecognized script type: %s", configuration.ScriptType()))
113+
log.Panic(fmt.Sprintf("Unrecognized script type: %s", accountConfiguration.ScriptType()))
109114
}
110115

111116
return &AccountAddress{
112117
Address: address,
113118
AccountConfiguration: accountConfiguration,
114-
Configuration: configuration,
119+
publicKey: publicKey,
115120
Derivation: derivation,
116121
redeemScript: redeemScript,
117122
log: log,
@@ -128,8 +133,8 @@ func (address *AccountAddress) ID() string {
128133
// - 32 byte x-only public key for p2tr
129134
// See https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#user-content-Inputs_For_Shared_Secret_Derivation.
130135
func (address *AccountAddress) BIP352Pubkey() ([]byte, error) {
131-
publicKey := address.Configuration.PublicKey()
132-
switch address.Configuration.ScriptType() {
136+
publicKey := address.publicKey
137+
switch address.AccountConfiguration.ScriptType() {
133138
case signing.ScriptTypeP2PKH, signing.ScriptTypeP2WPKHP2SH, signing.ScriptTypeP2WPKH:
134139
return publicKey.SerializeCompressed(), nil
135140
case signing.ScriptTypeP2TR:
@@ -145,9 +150,11 @@ func (address *AccountAddress) EncodeForHumans() string {
145150
return address.EncodeAddress()
146151
}
147152

148-
// AbsoluteKeypath implements coin.AbsoluteKeypath.
153+
// AbsoluteKeypath implements accounts.Address.
149154
func (address *AccountAddress) AbsoluteKeypath() signing.AbsoluteKeypath {
150-
return address.Configuration.AbsoluteKeypath()
155+
return address.AccountConfiguration.AbsoluteKeypath().
156+
Child(address.Derivation.SimpleChainIndex(), false).
157+
Child(address.Derivation.AddressIndex, false)
151158
}
152159

153160
// PubkeyScript returns the pubkey script of this address. Use this in a tx output to receive funds.
@@ -169,7 +176,7 @@ func (address *AccountAddress) PubkeyScriptHashHex() blockchain.ScriptHashHex {
169176
// calculating the hash to be signed in a transaction. This info is needed when trying to spend
170177
// from this address.
171178
func (address *AccountAddress) ScriptForHashToSign() (bool, []byte) {
172-
switch address.Configuration.ScriptType() {
179+
switch address.AccountConfiguration.ScriptType() {
173180
case signing.ScriptTypeP2PKH:
174181
return false, address.PubkeyScript()
175182
case signing.ScriptTypeP2WPKHP2SH:
@@ -187,8 +194,8 @@ func (address *AccountAddress) ScriptForHashToSign() (bool, []byte) {
187194
func (address *AccountAddress) SignatureScript(
188195
signature types.Signature,
189196
) ([]byte, wire.TxWitness) {
190-
publicKey := address.Configuration.PublicKey()
191-
switch address.Configuration.ScriptType() {
197+
publicKey := address.publicKey
198+
switch address.AccountConfiguration.ScriptType() {
192199
case signing.ScriptTypeP2PKH:
193200
signatureScript, err := txscript.NewScriptBuilder().
194201
AddData(append(signature.SerializeDER(), byte(txscript.SigHashAll))).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2025 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package addresses
16+
17+
import "github.com/btcsuite/btcd/btcec/v2"
18+
19+
// TstPublicKey exports the publickey for use in unit tests.
20+
func (address *AccountAddress) TstPublicKey() *btcec.PublicKey {
21+
return address.publicKey
22+
}

backend/coins/btc/addresses/address_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (s *addressTestSuite) TestNewAddress() {
5757
Child(10, false).
5858
Child(0, false).
5959
Child(0, false)
60-
s.Require().Equal(expectedKeypath, s.address.Configuration.AbsoluteKeypath())
60+
s.Require().Equal(expectedKeypath, s.address.AbsoluteKeypath())
6161
s.Require().Equal("moTM88EgqzATgCjSrcNfahXaT9uCy3FHh3", s.address.EncodeAddress())
6262
s.Require().True(s.address.IsForNet(net))
6363
}

backend/coins/btc/addresses/addresschain_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ func (s *addressChainTestSuite) TestEnsureAddresses() {
142142
}
143143
s.Require().Len(newAddresses, s.gapLimit)
144144
for index, address := range newAddresses {
145-
s.Require().Equal(uint32(index), address.Configuration.AbsoluteKeypath().ToUInt32()[1])
146-
s.Require().Equal(getPubKey(index), address.Configuration.PublicKey())
145+
s.Require().Equal(uint32(index), address.AbsoluteKeypath().ToUInt32()[1])
146+
s.Require().Equal(getPubKey(index), address.TstPublicKey())
147147
}
148148
// Address statuses are still the same, so calling it again won't produce more addresses.
149149
addrs, err := s.addresses.EnsureAddresses()
@@ -157,7 +157,7 @@ func (s *addressChainTestSuite) TestEnsureAddresses() {
157157
moreAddresses, err := s.addresses.EnsureAddresses()
158158
s.Require().NoError(err)
159159
s.Require().Len(moreAddresses, s.gapLimit)
160-
s.Require().Equal(uint32(s.gapLimit), moreAddresses[0].Configuration.AbsoluteKeypath().ToUInt32()[1])
160+
s.Require().Equal(uint32(s.gapLimit), moreAddresses[0].Derivation.AddressIndex)
161161

162162
// Repeating it does not add more the unused addresses are the same.
163163
addrs, err = s.addresses.EnsureAddresses()

backend/coins/btc/handlers/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ func (handlers *Handlers) getUTXOs(*http.Request) (interface{}, error) {
380380
"txOutput": output.OutPoint.Index,
381381
"amount": handlers.formatBTCAmountAsJSON(btcutil.Amount(output.TxOut.Value), false),
382382
"address": address,
383-
"scriptType": output.Address.Configuration.ScriptType(),
383+
"scriptType": output.Address.AccountConfiguration.ScriptType(),
384384
"note": handlers.account.TxNote(output.OutPoint.Hash.String()),
385385
"addressReused": addressReused,
386386
"isChange": output.IsChange,

backend/coins/btc/maketx/maketx.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func toInputConfigurations(
127127
) []*signing.Configuration {
128128
inputConfigurations := make([]*signing.Configuration, len(selectedOutPoints))
129129
for i, outPoint := range selectedOutPoints {
130-
inputConfigurations[i] = spendableOutputs[outPoint].Address.Configuration
130+
inputConfigurations[i] = spendableOutputs[outPoint].Address.AccountConfiguration
131131
}
132132
return inputConfigurations
133133
}
@@ -277,7 +277,7 @@ func NewTx(
277277
}
278278
changeAmount := selectedOutputsSum - targetAmount - maxRequiredFee
279279
changeIsDust := isDustAmount(
280-
changeAmount, len(changePKScript), changeAddress.Configuration, feePerKb)
280+
changeAmount, len(changePKScript), changeAddress.AccountConfiguration, feePerKb)
281281
finalFee := maxRequiredFee
282282
if changeIsDust {
283283
log.Info("change is dust")

backend/coins/btc/maketx/txsize_internal_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func testEstimateTxSize(
9292
Witness: witness,
9393
Sequence: 0,
9494
})
95-
inputConfigurations = append(inputConfigurations, inputAddress.Configuration)
95+
inputConfigurations = append(inputConfigurations, inputAddress.AccountConfiguration)
9696
}
9797
}
9898
changePkScriptSize := 0
@@ -119,8 +119,8 @@ func TestSigScriptWitnessSize(t *testing.T) {
119119
// Test all singlesig configurations.
120120
for _, scriptType := range scriptTypes {
121121
address := addressesTest.GetAddress(scriptType)
122-
t.Run(address.Configuration.String(), func(t *testing.T) {
123-
sigScriptSize, witnessSize := sigScriptWitnessSize(address.Configuration)
122+
t.Run(address.AccountConfiguration.String(), func(t *testing.T) {
123+
sigScriptSize, witnessSize := sigScriptWitnessSize(address.AccountConfiguration)
124124
sigScript, witness := address.SignatureScript(sig)
125125
require.Equal(t, len(sigScript), sigScriptSize)
126126
if witness != nil {

backend/coins/btc/transaction.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (account *Account) pickChangeAddress(utxos map[wire.OutPoint]maketx.UTXO) (
104104
if p2trIndex >= 0 {
105105
// Check if there is at least one taproot UTXO.
106106
for _, utxo := range utxos {
107-
if utxo.Address.Configuration.ScriptType() == signing.ScriptTypeP2TR {
107+
if utxo.Address.AccountConfiguration.ScriptType() == signing.ScriptTypeP2TR {
108108
// Found a taproot UTXO.
109109
unusedAddresses, err := account.subaccounts[p2trIndex].changeAddresses.GetUnused()
110110
if err != nil {
@@ -211,7 +211,7 @@ func (account *Account) newTx(args *accounts.TxProposalArgs) (
211211
if err != nil {
212212
return nil, nil, err
213213
}
214-
account.log.Infof("Change address script type: %s", changeAddress.Configuration.ScriptType())
214+
account.log.Infof("Change address script type: %s", changeAddress.AccountConfiguration.ScriptType())
215215
txProposal, err = maketx.NewTx(
216216
account.coin,
217217
wireUTXO,

backend/coins/btc/transaction_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func TestGetFeePerKb(t *testing.T) {
167167
func utxo(scriptType signing.ScriptType) maketx.UTXO {
168168
return maketx.UTXO{
169169
Address: &addresses.AccountAddress{
170-
Configuration: &signing.Configuration{
170+
AccountConfiguration: &signing.Configuration{
171171
BitcoinSimple: &signing.BitcoinSimple{
172172
ScriptType: scriptType,
173173
},

backend/devices/bitbox02/keystore.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ func (keystore *keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact
356356
PrevOutIndex: txIn.PreviousOutPoint.Index,
357357
PrevOutValue: uint64(prevOut.TxOut.Value),
358358
Sequence: txIn.Sequence,
359-
Keypath: inputAddress.Configuration.AbsoluteKeypath().ToUInt32(),
359+
Keypath: inputAddress.AbsoluteKeypath().ToUInt32(),
360360
ScriptConfigIndex: uint32(scriptConfigIndex),
361361
},
362362
BIP352Pubkey: bip352Pubkey,
@@ -444,13 +444,13 @@ func (keystore *keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact
444444
}
445445
switch {
446446
case sameAccount:
447-
keypath = outputAddress.Configuration.AbsoluteKeypath().ToUInt32()
447+
keypath = outputAddress.AbsoluteKeypath().ToUInt32()
448448
scriptConfigIndex = addScriptConfig(&messages.BTCScriptConfigWithKeypath{
449449
ScriptConfig: firmware.NewBTCScriptConfigSimple(msgScriptType),
450450
Keypath: accountConfiguration.AbsoluteKeypath().ToUInt32(),
451451
})
452452
case keystore.device.Version().AtLeast(semver.NewSemVer(9, 22, 0)):
453-
keypath = outputAddress.Configuration.AbsoluteKeypath().ToUInt32()
453+
keypath = outputAddress.AbsoluteKeypath().ToUInt32()
454454
outputScriptConfigIdx := addOutputScriptConfig(&messages.BTCScriptConfigWithKeypath{
455455
ScriptConfig: firmware.NewBTCScriptConfigSimple(msgScriptType),
456456
Keypath: accountConfiguration.AbsoluteKeypath().ToUInt32(),

backend/keystore/software/software.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func (keystore *Keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact
203203
return err
204204
}
205205

206-
xprv, err := address.Configuration.AbsoluteKeypath().Derive(keystore.master)
206+
xprv, err := address.AbsoluteKeypath().Derive(keystore.master)
207207
if err != nil {
208208
return err
209209
}
@@ -212,7 +212,7 @@ func (keystore *Keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact
212212
return errp.WithStack(err)
213213
}
214214

215-
if address.Configuration.ScriptType() == signing.ScriptTypeP2TR {
215+
if address.AccountConfiguration.ScriptType() == signing.ScriptTypeP2TR {
216216
prv = txscript.TweakTaprootPrivKey(*prv, nil)
217217
signatureHash, err := txscript.CalcTaprootSignatureHash(
218218
sigHashes, txscript.SigHashDefault, transaction,

backend/signing/configuration.go

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -189,29 +189,6 @@ func (configuration *Configuration) PublicKey() *btcec.PublicKey {
189189
return publicKey
190190
}
191191

192-
// Derive derives a subkeypath from the configuration's base absolute keypath.
193-
func (configuration *Configuration) Derive(relativeKeypath RelativeKeypath) (*Configuration, error) {
194-
btc := configuration.BitcoinSimple
195-
if btc != nil {
196-
if relativeKeypath.Hardened() {
197-
return nil, errp.New("A configuration can only be derived with a non-hardened relative keypath.")
198-
}
199-
200-
derivedPublicKey, err := relativeKeypath.Derive(btc.KeyInfo.ExtendedPublicKey)
201-
if err != nil {
202-
return nil, err
203-
}
204-
return NewBitcoinConfiguration(
205-
btc.ScriptType,
206-
btc.KeyInfo.RootFingerprint,
207-
btc.KeyInfo.AbsoluteKeypath.Append(relativeKeypath),
208-
derivedPublicKey,
209-
), nil
210-
}
211-
212-
return nil, errp.New("Can only call this on a bitcoin configuration")
213-
}
214-
215192
// String returns a short summary of the configuration to be used in logs, etc.
216193
func (configuration *Configuration) String() string {
217194
if configuration.BitcoinSimple != nil {

0 commit comments

Comments
 (0)