Skip to content

Continue with BTC address derivation using account config + derivation info #3335

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

Merged
merged 4 commits into from
May 20, 2025
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
5 changes: 4 additions & 1 deletion backend/coins/btc/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,10 @@ func (account *Account) VerifyAddress(addressID string) (bool, error) {
return false, err
}
if canVerifyAddress {
return true, keystore.VerifyAddress(address.Configuration, account.Coin())
return true, keystore.VerifyAddressBTC(
address.AccountConfiguration,
address.Derivation,
account.Coin())
}
return false, nil
}
Expand Down
57 changes: 33 additions & 24 deletions backend/coins/btc/addresses/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
ourbtcutil "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/util"
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/signing"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
Expand All @@ -37,8 +38,9 @@ type AccountAddress struct {

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

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

var address btcutil.Address
var redeemScript []byte
configuration, err := accountConfiguration.Derive(
signing.NewEmptyRelativeKeypath().
Child(derivation.SimpleChainIndex(), signing.NonHardened).
Child(derivation.AddressIndex, signing.NonHardened),
)
if err != nil {
log.WithError(err).Panic("Failed to derive the configuration.")
}
log = log.WithFields(logrus.Fields{
"accountConfiguration": accountConfiguration.String(),
"change": derivation.Change,
"addressIndex": derivation.AddressIndex,
})
log.Debug("Creating new account address")

publicKeyHash := btcutil.Hash160(configuration.PublicKey().SerializeCompressed())
switch configuration.ScriptType() {
var address btcutil.Address
var redeemScript []byte
relativeKeypath := signing.NewEmptyRelativeKeypath().
Child(derivation.SimpleChainIndex(), signing.NonHardened).
Child(derivation.AddressIndex, signing.NonHardened)
derivedXpub, err := relativeKeypath.Derive(accountConfiguration.ExtendedPublicKey())
if err != nil {
log.WithError(err).Panic("Failed to derive xpub.")
}
publicKey, err := derivedXpub.ECPubKey()
if err != nil {
log.WithError(err).Panic("Failed to convert an extended public key to a normal public key.")
}

publicKeyHash := btcutil.Hash160(publicKey.SerializeCompressed())
switch accountConfiguration.ScriptType() {
case signing.ScriptTypeP2PKH:
address, err = btcutil.NewAddressPubKeyHash(publicKeyHash, net)
if err != nil {
Expand All @@ -98,19 +104,20 @@ func NewAccountAddress(
log.WithError(err).Panic("Failed to get p2wpkh addr. from publ. key hash.")
}
case signing.ScriptTypeP2TR:
outputKey := txscript.ComputeTaprootKeyNoScript(configuration.PublicKey())
outputKey := txscript.ComputeTaprootKeyNoScript(publicKey)
address, err = btcutil.NewAddressTaproot(schnorr.SerializePubKey(outputKey), net)
if err != nil {
log.WithError(err).Panic("Failed to get p2tr addr")
}
default:
log.Panic(fmt.Sprintf("Unrecognized script type: %s", configuration.ScriptType()))
log.Panic(fmt.Sprintf("Unrecognized script type: %s", accountConfiguration.ScriptType()))
}

return &AccountAddress{
Address: address,
AccountConfiguration: accountConfiguration,
Configuration: configuration,
publicKey: publicKey,
Derivation: derivation,
redeemScript: redeemScript,
log: log,
}
Expand All @@ -126,8 +133,8 @@ func (address *AccountAddress) ID() string {
// - 32 byte x-only public key for p2tr
// See https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#user-content-Inputs_For_Shared_Secret_Derivation.
func (address *AccountAddress) BIP352Pubkey() ([]byte, error) {
publicKey := address.Configuration.PublicKey()
switch address.Configuration.ScriptType() {
publicKey := address.publicKey
switch address.AccountConfiguration.ScriptType() {
case signing.ScriptTypeP2PKH, signing.ScriptTypeP2WPKHP2SH, signing.ScriptTypeP2WPKH:
return publicKey.SerializeCompressed(), nil
case signing.ScriptTypeP2TR:
Expand All @@ -143,9 +150,11 @@ func (address *AccountAddress) EncodeForHumans() string {
return address.EncodeAddress()
}

// AbsoluteKeypath implements coin.AbsoluteKeypath.
// AbsoluteKeypath implements accounts.Address.
func (address *AccountAddress) AbsoluteKeypath() signing.AbsoluteKeypath {
return address.Configuration.AbsoluteKeypath()
return address.AccountConfiguration.AbsoluteKeypath().
Child(address.Derivation.SimpleChainIndex(), false).
Child(address.Derivation.AddressIndex, false)
}

// PubkeyScript returns the pubkey script of this address. Use this in a tx output to receive funds.
Expand All @@ -167,7 +176,7 @@ func (address *AccountAddress) PubkeyScriptHashHex() blockchain.ScriptHashHex {
// calculating the hash to be signed in a transaction. This info is needed when trying to spend
// from this address.
func (address *AccountAddress) ScriptForHashToSign() (bool, []byte) {
switch address.Configuration.ScriptType() {
switch address.AccountConfiguration.ScriptType() {
case signing.ScriptTypeP2PKH:
return false, address.PubkeyScript()
case signing.ScriptTypeP2WPKHP2SH:
Expand All @@ -185,8 +194,8 @@ func (address *AccountAddress) ScriptForHashToSign() (bool, []byte) {
func (address *AccountAddress) SignatureScript(
signature types.Signature,
) ([]byte, wire.TxWitness) {
publicKey := address.Configuration.PublicKey()
switch address.Configuration.ScriptType() {
publicKey := address.publicKey
switch address.AccountConfiguration.ScriptType() {
case signing.ScriptTypeP2PKH:
signatureScript, err := txscript.NewScriptBuilder().
AddData(append(signature.SerializeDER(), byte(txscript.SigHashAll))).
Expand Down
22 changes: 22 additions & 0 deletions backend/coins/btc/addresses/address_export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2025 Shift Crypto AG
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package addresses

import "github.com/btcsuite/btcd/btcec/v2"

// TstPublicKey exports the publickey for use in unit tests.
func (address *AccountAddress) TstPublicKey() *btcec.PublicKey {
return address.publicKey
}
2 changes: 1 addition & 1 deletion backend/coins/btc/addresses/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (s *addressTestSuite) TestNewAddress() {
Child(10, false).
Child(0, false).
Child(0, false)
s.Require().Equal(expectedKeypath, s.address.Configuration.AbsoluteKeypath())
s.Require().Equal(expectedKeypath, s.address.AbsoluteKeypath())
s.Require().Equal("moTM88EgqzATgCjSrcNfahXaT9uCy3FHh3", s.address.EncodeAddress())
s.Require().True(s.address.IsForNet(net))
}
Expand Down
6 changes: 3 additions & 3 deletions backend/coins/btc/addresses/addresschain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ func (s *addressChainTestSuite) TestEnsureAddresses() {
}
s.Require().Len(newAddresses, s.gapLimit)
for index, address := range newAddresses {
s.Require().Equal(uint32(index), address.Configuration.AbsoluteKeypath().ToUInt32()[1])
s.Require().Equal(getPubKey(index), address.Configuration.PublicKey())
s.Require().Equal(uint32(index), address.AbsoluteKeypath().ToUInt32()[1])
s.Require().Equal(getPubKey(index), address.TstPublicKey())
}
// Address statuses are still the same, so calling it again won't produce more addresses.
addrs, err := s.addresses.EnsureAddresses()
Expand All @@ -157,7 +157,7 @@ func (s *addressChainTestSuite) TestEnsureAddresses() {
moreAddresses, err := s.addresses.EnsureAddresses()
s.Require().NoError(err)
s.Require().Len(moreAddresses, s.gapLimit)
s.Require().Equal(uint32(s.gapLimit), moreAddresses[0].Configuration.AbsoluteKeypath().ToUInt32()[1])
s.Require().Equal(uint32(s.gapLimit), moreAddresses[0].Derivation.AddressIndex)

// Repeating it does not add more the unused addresses are the same.
addrs, err = s.addresses.EnsureAddresses()
Expand Down
2 changes: 1 addition & 1 deletion backend/coins/btc/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ func (handlers *Handlers) getUTXOs(*http.Request) (interface{}, error) {
"txOutput": output.OutPoint.Index,
"amount": handlers.formatBTCAmountAsJSON(btcutil.Amount(output.TxOut.Value), false),
"address": address,
"scriptType": output.Address.Configuration.ScriptType(),
"scriptType": output.Address.AccountConfiguration.ScriptType(),
"note": handlers.account.TxNote(output.OutPoint.Hash.String()),
"addressReused": addressReused,
"isChange": output.IsChange,
Expand Down
4 changes: 2 additions & 2 deletions backend/coins/btc/maketx/maketx.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func toInputConfigurations(
) []*signing.Configuration {
inputConfigurations := make([]*signing.Configuration, len(selectedOutPoints))
for i, outPoint := range selectedOutPoints {
inputConfigurations[i] = spendableOutputs[outPoint].Address.Configuration
inputConfigurations[i] = spendableOutputs[outPoint].Address.AccountConfiguration
}
return inputConfigurations
}
Expand Down Expand Up @@ -277,7 +277,7 @@ func NewTx(
}
changeAmount := selectedOutputsSum - targetAmount - maxRequiredFee
changeIsDust := isDustAmount(
changeAmount, len(changePKScript), changeAddress.Configuration, feePerKb)
changeAmount, len(changePKScript), changeAddress.AccountConfiguration, feePerKb)
finalFee := maxRequiredFee
if changeIsDust {
log.Info("change is dust")
Expand Down
6 changes: 3 additions & 3 deletions backend/coins/btc/maketx/txsize_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func testEstimateTxSize(
Witness: witness,
Sequence: 0,
})
inputConfigurations = append(inputConfigurations, inputAddress.Configuration)
inputConfigurations = append(inputConfigurations, inputAddress.AccountConfiguration)
}
}
changePkScriptSize := 0
Expand All @@ -119,8 +119,8 @@ func TestSigScriptWitnessSize(t *testing.T) {
// Test all singlesig configurations.
for _, scriptType := range scriptTypes {
address := addressesTest.GetAddress(scriptType)
t.Run(address.Configuration.String(), func(t *testing.T) {
sigScriptSize, witnessSize := sigScriptWitnessSize(address.Configuration)
t.Run(address.AccountConfiguration.String(), func(t *testing.T) {
sigScriptSize, witnessSize := sigScriptWitnessSize(address.AccountConfiguration)
sigScript, witness := address.SignatureScript(sig)
require.Equal(t, len(sigScript), sigScriptSize)
if witness != nil {
Expand Down
4 changes: 2 additions & 2 deletions backend/coins/btc/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (account *Account) pickChangeAddress(utxos map[wire.OutPoint]maketx.UTXO) (
if p2trIndex >= 0 {
// Check if there is at least one taproot UTXO.
for _, utxo := range utxos {
if utxo.Address.Configuration.ScriptType() == signing.ScriptTypeP2TR {
if utxo.Address.AccountConfiguration.ScriptType() == signing.ScriptTypeP2TR {
// Found a taproot UTXO.
unusedAddresses, err := account.subaccounts[p2trIndex].changeAddresses.GetUnused()
if err != nil {
Expand Down Expand Up @@ -211,7 +211,7 @@ func (account *Account) newTx(args *accounts.TxProposalArgs) (
if err != nil {
return nil, nil, err
}
account.log.Infof("Change address script type: %s", changeAddress.Configuration.ScriptType())
account.log.Infof("Change address script type: %s", changeAddress.AccountConfiguration.ScriptType())
txProposal, err = maketx.NewTx(
account.coin,
wireUTXO,
Expand Down
2 changes: 1 addition & 1 deletion backend/coins/btc/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func TestGetFeePerKb(t *testing.T) {
func utxo(scriptType signing.ScriptType) maketx.UTXO {
return maketx.UTXO{
Address: &addresses.AccountAddress{
Configuration: &signing.Configuration{
AccountConfiguration: &signing.Configuration{
BitcoinSimple: &signing.BitcoinSimple{
ScriptType: scriptType,
},
Expand Down
2 changes: 1 addition & 1 deletion backend/coins/eth/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ func (account *Account) VerifyAddress(addressID string) (bool, error) {
return false, err
}
if canVerifyAddress {
return true, keystore.VerifyAddress(account.signingConfiguration, account.Coin())
return true, keystore.VerifyAddressETH(account.signingConfiguration, account.Coin())
}
return false, nil
}
Expand Down
Loading