Skip to content
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
110 changes: 110 additions & 0 deletions backend/account_derivation_spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: Apache-2.0

package backend

import (
coinpkg "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/signing"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
)

type accountDerivationKind int

const (
accountDerivationKindBTC accountDerivationKind = iota
accountDerivationKindETH
)

type scriptTypeWithKeypath struct {
scriptType signing.ScriptType
keypath signing.AbsoluteKeypath
}

type accountDerivationSpec struct {
kind accountDerivationKind
btcConfigs []scriptTypeWithKeypath
ethKeypath signing.AbsoluteKeypath
}

// newAccountDerivationSpec returns the standard account-level derivation for a coin.
func newAccountDerivationSpec(coinCode coinpkg.Code, accountNumber uint16) (accountDerivationSpec, error) {
coinType, ok := coinpkg.BIP44CoinType(coinCode)
if !ok {
return accountDerivationSpec{}, errp.Newf("Unrecognized coin code: %s", coinCode)
}

switch coinCode {
case coinpkg.CodeBTC, coinpkg.CodeTBTC, coinpkg.CodeRBTC:
return bitcoinAccountDerivationSpec(coinType, accountNumber), nil
case coinpkg.CodeLTC, coinpkg.CodeTLTC:
return litecoinAccountDerivationSpec(coinType, accountNumber), nil
case coinpkg.CodeETH, coinpkg.CodeSEPETH:
return ethereumAccountDerivationSpec(coinType, accountNumber), nil
default:
return accountDerivationSpec{}, errp.Newf("Unrecognized coin code: %s", coinCode)
}
}

// bitcoinAccountDerivationSpec returns the BTC account configurations in persistence order.
func bitcoinAccountDerivationSpec(
coinType uint32,
accountNumber uint16,
) accountDerivationSpec {
return accountDerivationSpec{
kind: accountDerivationKindBTC,
btcConfigs: []scriptTypeWithKeypath{
btcScriptDerivation(signing.ScriptTypeP2WPKH, 84, coinType, accountNumber),
btcScriptDerivation(signing.ScriptTypeP2TR, 86, coinType, accountNumber),
btcScriptDerivation(signing.ScriptTypeP2WPKHP2SH, 49, coinType, accountNumber),
btcScriptDerivation(signing.ScriptTypeP2PKH, 44, coinType, accountNumber),
},
}
}

// litecoinAccountDerivationSpec returns the LTC account configurations in persistence order.
func litecoinAccountDerivationSpec(
coinType uint32,
accountNumber uint16,
) accountDerivationSpec {
return accountDerivationSpec{
kind: accountDerivationKindBTC,
btcConfigs: []scriptTypeWithKeypath{
btcScriptDerivation(signing.ScriptTypeP2WPKH, 84, coinType, accountNumber),
btcScriptDerivation(signing.ScriptTypeP2WPKHP2SH, 49, coinType, accountNumber),
},
}
}

// ethereumAccountDerivationSpec returns the ETH account derivation path.
func ethereumAccountDerivationSpec(
coinType uint32,
accountNumber uint16,
) accountDerivationSpec {
return accountDerivationSpec{
kind: accountDerivationKindETH,
ethKeypath: signing.NewAbsoluteKeypathFromUint32(
44+hardenedKeystart,
coinType+hardenedKeystart,
hardenedKeystart,
0,
uint32(accountNumber),
),
}
}

// btcScriptDerivation returns one script-specific account derivation.
func btcScriptDerivation(
scriptType signing.ScriptType,
purpose uint32,
coinType uint32,
accountNumber uint16,
) scriptTypeWithKeypath {
return scriptTypeWithKeypath{
scriptType: scriptType,
keypath: signing.NewAbsoluteKeypathFromUint32(
purpose+hardenedKeystart,
coinType+hardenedKeystart,
uint32(accountNumber)+hardenedKeystart,
),
}
}
129 changes: 129 additions & 0 deletions backend/account_derivation_spec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: Apache-2.0

package backend

import (
"testing"

coinpkg "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/signing"
"github.com/stretchr/testify/require"
)

type expectedScriptDerivation struct {
scriptType signing.ScriptType
keypath string
}

func TestNewAccountDerivationSpecBitcoinLike(t *testing.T) {
tests := []struct {
name string
coinCode coinpkg.Code
expected []expectedScriptDerivation
}{
{
name: "btc",
coinCode: coinpkg.CodeBTC,
expected: []expectedScriptDerivation{
{signing.ScriptTypeP2WPKH, "m/84'/0'/7'"},
{signing.ScriptTypeP2TR, "m/86'/0'/7'"},
{signing.ScriptTypeP2WPKHP2SH, "m/49'/0'/7'"},
{signing.ScriptTypeP2PKH, "m/44'/0'/7'"},
},
},
{
name: "tbtc",
coinCode: coinpkg.CodeTBTC,
expected: []expectedScriptDerivation{
{signing.ScriptTypeP2WPKH, "m/84'/1'/7'"},
{signing.ScriptTypeP2TR, "m/86'/1'/7'"},
{signing.ScriptTypeP2WPKHP2SH, "m/49'/1'/7'"},
{signing.ScriptTypeP2PKH, "m/44'/1'/7'"},
},
},
{
name: "rbtc",
coinCode: coinpkg.CodeRBTC,
expected: []expectedScriptDerivation{
{signing.ScriptTypeP2WPKH, "m/84'/1'/7'"},
{signing.ScriptTypeP2TR, "m/86'/1'/7'"},
{signing.ScriptTypeP2WPKHP2SH, "m/49'/1'/7'"},
{signing.ScriptTypeP2PKH, "m/44'/1'/7'"},
},
},
{
name: "ltc",
coinCode: coinpkg.CodeLTC,
expected: []expectedScriptDerivation{
{signing.ScriptTypeP2WPKH, "m/84'/2'/7'"},
{signing.ScriptTypeP2WPKHP2SH, "m/49'/2'/7'"},
},
},
{
name: "tltc",
coinCode: coinpkg.CodeTLTC,
expected: []expectedScriptDerivation{
{signing.ScriptTypeP2WPKH, "m/84'/1'/7'"},
{signing.ScriptTypeP2WPKHP2SH, "m/49'/1'/7'"},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
spec, err := newAccountDerivationSpec(test.coinCode, 7)
require.NoError(t, err)
require.Equal(t, accountDerivationKindBTC, spec.kind)
require.Empty(t, spec.ethKeypath)
requireScriptDerivations(t, test.expected, spec.btcConfigs)
})
}
}

func TestNewAccountDerivationSpecEthereum(t *testing.T) {
tests := []struct {
name string
coinCode coinpkg.Code
keypath string
}{
{
name: "eth",
coinCode: coinpkg.CodeETH,
keypath: "m/44'/60'/0'/0/7",
},
{
name: "sepeth",
coinCode: coinpkg.CodeSEPETH,
keypath: "m/44'/1'/0'/0/7",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
spec, err := newAccountDerivationSpec(test.coinCode, 7)
require.NoError(t, err)
require.Equal(t, accountDerivationKindETH, spec.kind)
require.Empty(t, spec.btcConfigs)
require.Equal(t, test.keypath, spec.ethKeypath.Encode())
})
}
}

func TestNewAccountDerivationSpecUnsupportedCoin(t *testing.T) {
_, err := newAccountDerivationSpec(coinpkg.Code("doge"), 0)
require.EqualError(t, err, "Unrecognized coin code: doge")
}

func requireScriptDerivations(
t *testing.T,
expected []expectedScriptDerivation,
actual []scriptTypeWithKeypath,
) {
t.Helper()

require.Len(t, actual, len(expected))
for i, expectedDerivation := range expected {
require.Equal(t, expectedDerivation.scriptType, actual[i].scriptType)
require.Equal(t, expectedDerivation.keypath, actual[i].keypath.Encode())
}
}
76 changes: 21 additions & 55 deletions backend/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,55 +600,30 @@ func (backend *Backend) createAndPersistAccountConfig(
WithField("coinCode", coinCode).
WithField("accountNumber", accountNumber)
log.Info("Persisting new account config")
accountNumberHardened := uint32(accountNumber) + hardenedKeystart

switch coinCode {
case coinpkg.CodeBTC, coinpkg.CodeTBTC, coinpkg.CodeRBTC:
bip44Coin := 1 + hardenedKeystart
if coinCode == coinpkg.CodeBTC {
bip44Coin = hardenedKeystart
}
return accountCode, backend.persistBTCAccountConfig(keystore, accountCoin,
accountCode,
hiddenBecauseUnused,
name,
[]scriptTypeWithKeypath{
{signing.ScriptTypeP2WPKH, signing.NewAbsoluteKeypathFromUint32(84+hardenedKeystart, bip44Coin, accountNumberHardened)},
{signing.ScriptTypeP2TR, signing.NewAbsoluteKeypathFromUint32(86+hardenedKeystart, bip44Coin, accountNumberHardened)},
{signing.ScriptTypeP2WPKHP2SH, signing.NewAbsoluteKeypathFromUint32(49+hardenedKeystart, bip44Coin, accountNumberHardened)},
{signing.ScriptTypeP2PKH, signing.NewAbsoluteKeypathFromUint32(44+hardenedKeystart, bip44Coin, accountNumberHardened)},
},
accountsConfig,
)
case coinpkg.CodeLTC, coinpkg.CodeTLTC:
bip44Coin := 1 + hardenedKeystart
if coinCode == coinpkg.CodeLTC {
bip44Coin = 2 + hardenedKeystart
}
derivationSpec, err := newAccountDerivationSpec(coinCode, accountNumber)
if err != nil {
return "", err
}

switch derivationSpec.kind {
case accountDerivationKindBTC:
return accountCode, backend.persistBTCAccountConfig(keystore, accountCoin,
accountCode,
hiddenBecauseUnused,
name,
[]scriptTypeWithKeypath{
{signing.ScriptTypeP2WPKH, signing.NewAbsoluteKeypathFromUint32(84+hardenedKeystart, bip44Coin, accountNumberHardened)},
{signing.ScriptTypeP2WPKHP2SH, signing.NewAbsoluteKeypathFromUint32(49+hardenedKeystart, bip44Coin, accountNumberHardened)},
},
derivationSpec.btcConfigs,
accountsConfig,
)
case coinpkg.CodeETH, coinpkg.CodeSEPETH:
bip44Coin := "1'"
if coinCode == coinpkg.CodeETH {
bip44Coin = "60'"
}
case accountDerivationKindETH:
return accountCode, backend.persistETHAccountConfig(
keystore, accountCoin, accountCode, hiddenBecauseUnused,
// TODO: Use []uint32 instead of a string keypath
fmt.Sprintf("m/44'/%s/0'/0/%d", bip44Coin, accountNumber),
derivationSpec.ethKeypath,
name,
activeTokens,
accountsConfig)
default:
return "", errp.Newf("Unrecognized coin code: %s", coinCode)
panic("unhandled account derivation kind")
}
}

Expand Down Expand Up @@ -1167,11 +1142,6 @@ func (backend *Backend) persistAccount(account config.Account, accountsConfig *c
return nil
}

type scriptTypeWithKeypath struct {
scriptType signing.ScriptType
keypath signing.AbsoluteKeypath
}

// adds a combined BTC account with the given script types.
func (backend *Backend) persistBTCAccountConfig(
keystore keystore.Keystore,
Expand Down Expand Up @@ -1235,27 +1205,23 @@ func (backend *Backend) persistETHAccountConfig(
coin coinpkg.Coin,
code accountsTypes.Code,
hiddenBecauseUnused bool,
keypath string,
keypath signing.AbsoluteKeypath,
name string,
activeTokens []string,
accountsConfig *config.AccountsConfig,
) error {
log := backend.log.
WithField("code", code).
WithField("name", name).
WithField("keypath", keypath)
WithField("keypath", keypath.Encode())

if !keystore.SupportsAccount(coin, nil) {
log.Info("skipping unsupported account")
return nil
}

log.Info("persist account")
absoluteKeypath, err := signing.NewAbsoluteKeypath(keypath)
if err != nil {
panic(err)
}
extendedPublicKey, err := keystore.ExtendedPublicKey(coin, absoluteKeypath)
extendedPublicKey, err := keystore.ExtendedPublicKey(coin, keypath)
if err != nil {
return err
}
Expand All @@ -1267,7 +1233,7 @@ func (backend *Backend) persistETHAccountConfig(
signingConfigurations := signing.Configurations{
signing.NewEthereumConfiguration(
rootFingerprint,
absoluteKeypath,
keypath,
extendedPublicKey,
),
}
Expand Down Expand Up @@ -1401,18 +1367,18 @@ func (backend *Backend) maybeAddP2TR(keystore keystore.Keystore, accounts []*con
if err != nil {
return err
}
bip44Coin := 1 + hardenedKeystart
if account.CoinCode == coinpkg.CodeBTC {
bip44Coin = hardenedKeystart
bip44Coin, ok := coinpkg.BIP44CoinType(account.CoinCode)
if !ok {
return errp.Newf("Unrecognized coin code: %s", account.CoinCode)
}
accountNumber, err := account.SigningConfigurations[0].AccountNumber()
if err != nil {
return err
}
keypath := signing.NewAbsoluteKeypathFromUint32(
86+hdkeychain.HardenedKeyStart,
bip44Coin,
uint32(accountNumber)+hdkeychain.HardenedKeyStart)
86+hardenedKeystart,
bip44Coin+hardenedKeystart,
uint32(accountNumber)+hardenedKeystart)
extendedPublicKey, err := keystore.ExtendedPublicKey(accountCoin, keypath)
if err != nil {
return err
Expand Down
Loading