Skip to content

Commit 4a64cac

Browse files
committed
Merge branch 'xpubcache'
2 parents 5334aaf + 463b1f6 commit 4a64cac

File tree

14 files changed

+534
-71
lines changed

14 files changed

+534
-71
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
77
## Firmware
88

99
### [Unreleased]
10+
- Increased performance when signing Bitcoin transactions
1011
- Warn if the transaction fee is higher than 10% of the coins sent
1112
- ETH Testnets: add Goerli and remove deprecated Rinkeby and Ropsten
1213

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# limitations under the License.
1616

1717
set(DBB-FIRMWARE-SOURCES
18+
${CMAKE_SOURCE_DIR}/src/bip32.c
1819
${CMAKE_SOURCE_DIR}/src/firmware_main_loop.c
1920
${CMAKE_SOURCE_DIR}/src/keystore.c
2021
${CMAKE_SOURCE_DIR}/src/random.c
@@ -271,6 +272,7 @@ add_custom_target(rust-bindgen
271272
--use-core
272273
--with-derive-default
273274
--ctypes-prefix util::c_types
275+
--allowlist-function bip32_derive_xpub
274276
--allowlist-function strftime
275277
--allowlist-function localtime
276278
--allowlist-function wally_free_string

src/bip32.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2023 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+
#include "bip32.h"
16+
17+
#include <string.h>
18+
#include <wally_bip32.h>
19+
20+
bool bip32_derive_xpub(
21+
const uint8_t* xpub78,
22+
const uint32_t* keypath,
23+
size_t keypath_len,
24+
uint8_t* xpub78_out)
25+
{
26+
if (keypath_len == 0) {
27+
memcpy(xpub78_out, xpub78, BIP32_SERIALIZED_LEN);
28+
return true;
29+
}
30+
31+
struct ext_key xpub = {0};
32+
if (bip32_key_unserialize(xpub78, BIP32_SERIALIZED_LEN, &xpub) != WALLY_OK) {
33+
return false;
34+
}
35+
struct ext_key derived_xpub = {0};
36+
if (bip32_key_from_parent_path(
37+
&xpub, keypath, keypath_len, BIP32_FLAG_KEY_PUBLIC, &derived_xpub) != WALLY_OK) {
38+
return false;
39+
}
40+
return bip32_key_serialize(
41+
&derived_xpub, BIP32_FLAG_KEY_PUBLIC, xpub78_out, BIP32_SERIALIZED_LEN) == WALLY_OK;
42+
}

src/bip32.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2023 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+
#ifndef _BIP32_H_
16+
#define _BIP32_H_
17+
18+
#include <stdbool.h>
19+
#include <stddef.h>
20+
#include <stdint.h>
21+
22+
#include <compiler_util.h>
23+
24+
USE_RESULT bool bip32_derive_xpub(
25+
const uint8_t* xpub78,
26+
const uint32_t* keypath,
27+
size_t keypath_len,
28+
uint8_t* xpub78_out);
29+
30+
#endif

src/rust/bitbox02-rust/src/bip32.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ impl Xpub {
5656
public_key: xpub[45..78].to_vec(),
5757
}))
5858
}
59+
5960
/// Serializes a protobuf XPub to bytes according to the BIP32 specification. If xpub_type is
6061
/// None, the four version bytes are skipped.
6162
pub fn serialize(&self, xpub_type: Option<XPubType>) -> Result<Vec<u8>, ()> {
@@ -100,6 +101,12 @@ impl Xpub {
100101
.into_string())
101102
}
102103

104+
/// Derives child xpub at the keypath. All keypath elements must be unhardened.
105+
pub fn derive(&self, keypath: &[u32]) -> Result<Self, ()> {
106+
let xpub_ser = self.serialize(Some(XPubType::Xpub))?;
107+
Xpub::from_bytes(&bitbox02::bip32::derive_xpub(&xpub_ser, keypath)?)
108+
}
109+
103110
/// Returns the 33 bytes secp256k1 compressed pubkey.
104111
pub fn public_key(&self) -> &[u8] {
105112
self.xpub.public_key.as_slice()
@@ -116,6 +123,17 @@ impl Xpub {
116123
pub fn pubkey_uncompressed(&self) -> Result<[u8; 65], ()> {
117124
bitbox02::keystore::secp256k1_pubkey_compressed_to_uncompressed(self.public_key())
118125
}
126+
127+
/// Return the tweaked taproot pubkey.
128+
///
129+
/// Instead of returning the original pubkey at the keypath directly, it is tweaked with the
130+
/// hash of the pubkey.
131+
///
132+
/// See
133+
/// https://github.com/bitcoin/bips/blob/edffe529056f6dfd33d8f716fb871467c3c09263/bip-0086.mediawiki#address-derivation
134+
pub fn schnorr_bip86_pubkey(&self) -> Result<[u8; 32], ()> {
135+
bitbox02::keystore::secp256k1_schnorr_bip86_pubkey(self.public_key())
136+
}
119137
}
120138

121139
/// Parses a Base58Check-encoded xpub string. The 4 version bytes are not checked and discarded.
@@ -178,6 +196,30 @@ mod tests {
178196
);
179197
}
180198

199+
#[test]
200+
fn test_derive() {
201+
let xpub_str = "xpub661MyMwAqRbcGpuMRXa55WgyqinF4dpxvqQK63xBHtnH5yK4e3cTLqbX9CP4mEMHUbqsjSQ8y3hhbAzuMhpn8eEiLNVSWYaVSbKMAtUPyYH";
202+
let xpub = Xpub::from(parse_xpub(xpub_str).unwrap());
203+
204+
assert_eq!(
205+
xpub.derive(&[])
206+
.unwrap()
207+
.serialize_str(XPubType::Xpub)
208+
.unwrap(),
209+
xpub_str,
210+
);
211+
let expected = "xpub6CYiDoWMtLVQNrc4tbAvuRk5wjsp6MFgtYEdBUV7TGLUjutavHdEKLu9KpTpRxEZULbSwM1UQPaQpqAhmWYvngXCGHGE7hSZFNofeSRzmk5";
212+
assert_eq!(
213+
xpub.derive(&[0, 1, 2])
214+
.unwrap()
215+
.serialize_str(XPubType::Xpub)
216+
.unwrap(),
217+
expected,
218+
);
219+
220+
assert!(xpub.derive(&[0, 1, util::bip32::HARDENED]).is_err());
221+
}
222+
181223
#[test]
182224
fn test_pubkey_hash160() {
183225
let xpub = Xpub::from(parse_xpub("xpub6GugPDcUhrSudznFss7wXvQV3gwFTEanxHdCyoNoHnZEr3PTbh2Fosg4JjfphaYAsqjBhmtTZ3Yo8tmGjSHtaPhExNiMCSvPzreqjrX4Wr7").unwrap());
@@ -201,4 +243,28 @@ mod tests {
201243
*b"\x04\x77\xa4\x4a\xa9\xe8\xc8\xfb\x51\x05\xef\x5e\xe2\x39\x4e\x8a\xed\x89\xad\x73\xfc\x74\x36\x14\x25\xf0\x63\x47\xec\xfe\x32\x61\x31\xe1\x33\x93\x67\xee\x3c\xbe\x87\x71\x92\x85\xa0\x7f\x77\x4b\x17\xeb\x93\x3e\xcf\x0b\x9b\x82\xac\xeb\xc1\x95\x22\x6d\x63\x42\x44",
202244
);
203245
}
246+
247+
#[test]
248+
fn test_schnorr_bip86_pubkey() {
249+
// Test vectors from:
250+
// https://github.com/bitcoin/bips/blob/edffe529056f6dfd33d8f716fb871467c3c09263/bip-0086.mediawiki#test-vectors
251+
// Here we only test the creation of the tweaked pubkkey. See `Payload::from_simple` for address generation.
252+
253+
let xpub = Xpub::from(parse_xpub("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap());
254+
255+
assert_eq!(
256+
xpub.derive(&[0, 0]).unwrap().schnorr_bip86_pubkey().unwrap(),
257+
*b"\xa6\x08\x69\xf0\xdb\xcf\x1d\xc6\x59\xc9\xce\xcb\xaf\x80\x50\x13\x5e\xa9\xe8\xcd\xc4\x87\x05\x3f\x1d\xc6\x88\x09\x49\xdc\x68\x4c",
258+
);
259+
260+
assert_eq!(
261+
xpub.derive(&[0, 1]).unwrap().schnorr_bip86_pubkey().unwrap(),
262+
*b"\xa8\x2f\x29\x94\x4d\x65\xb8\x6a\xe6\xb5\xe5\xcc\x75\xe2\x94\xea\xd6\xc5\x93\x91\xa1\xed\xc5\xe0\x16\xe3\x49\x8c\x67\xfc\x7b\xbb",
263+
);
264+
265+
assert_eq!(
266+
xpub.derive(&[1, 0]).unwrap().schnorr_bip86_pubkey().unwrap(),
267+
*b"\x88\x2d\x74\xe5\xd0\x57\x2d\x5a\x81\x6c\xef\x00\x41\xa9\x6b\x6c\x1d\xe8\x32\xf6\xf9\x67\x6d\x96\x05\xc4\x4d\x5e\x9a\x97\xd3\xdc",
268+
);
269+
}
204270
}

src/rust/bitbox02-rust/src/hww/api/bitcoin.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,13 @@ pub fn derive_address_simple(
134134
coin_params.taproot_support,
135135
)
136136
.or(Err(Error::InvalidInput))?;
137-
Ok(common::Payload::from_simple(coin_params, simple_type, keypath)?.address(coin_params)?)
137+
Ok(common::Payload::from_simple(
138+
&mut crate::xpubcache::XpubCache::new(),
139+
coin_params,
140+
simple_type,
141+
keypath,
142+
)?
143+
.address(coin_params)?)
138144
}
139145

140146
/// Processes a SimpleType (single-sig) adress api call.

src/rust/bitbox02-rust/src/hww/api/bitcoin/common.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use super::pb;
1616
use super::Error;
1717

18-
use crate::keystore;
18+
use crate::xpubcache::Bip32XpubCache;
1919

2020
use alloc::string::String;
2121
use alloc::vec::Vec;
@@ -71,17 +71,19 @@ pub struct Payload {
7171

7272
impl Payload {
7373
pub fn from_simple(
74+
xpub_cache: &mut Bip32XpubCache,
7475
params: &Params,
7576
simple_type: SimpleType,
7677
keypath: &[u32],
7778
) -> Result<Self, Error> {
7879
match simple_type {
7980
SimpleType::P2wpkh => Ok(Payload {
80-
data: keystore::get_xpub(keypath)?.pubkey_hash160(),
81+
data: xpub_cache.get_xpub(keypath)?.pubkey_hash160(),
8182
output_type: BtcOutputType::P2wpkh,
8283
}),
8384
SimpleType::P2wpkhP2sh => {
84-
let payload_p2wpkh = Payload::from_simple(params, SimpleType::P2wpkh, keypath)?;
85+
let payload_p2wpkh =
86+
Payload::from_simple(xpub_cache, params, SimpleType::P2wpkh, keypath)?;
8587
let pkscript_p2wpkh = payload_p2wpkh.pk_script(params)?;
8688
Ok(Payload {
8789
data: bitbox02::hash160(&pkscript_p2wpkh).to_vec(),
@@ -91,7 +93,10 @@ impl Payload {
9193
SimpleType::P2tr => {
9294
if params.taproot_support {
9395
Ok(Payload {
94-
data: keystore::secp256k1_schnorr_bip86_pubkey(keypath)?.to_vec(),
96+
data: xpub_cache
97+
.get_xpub(keypath)?
98+
.schnorr_bip86_pubkey()?
99+
.to_vec(),
95100
output_type: BtcOutputType::P2tr,
96101
})
97102
} else {
@@ -144,6 +149,7 @@ impl Payload {
144149
/// Computes the payload data from a script config. The payload can then be used generate a
145150
/// pkScript or an address.
146151
pub fn from(
152+
xpub_cache: &mut Bip32XpubCache,
147153
params: &Params,
148154
keypath: &[u32],
149155
script_config_account: &pb::BtcScriptConfigWithKeypath,
@@ -158,7 +164,7 @@ impl Payload {
158164
} => {
159165
let simple_type = pb::btc_script_config::SimpleType::from_i32(*simple_type)
160166
.ok_or(Error::InvalidInput)?;
161-
Self::from_simple(params, simple_type, keypath)
167+
Self::from_simple(xpub_cache, params, simple_type, keypath)
162168
}
163169
pb::BtcScriptConfigWithKeypath {
164170
script_config:
@@ -533,10 +539,12 @@ mod tests {
533539
mock_unlocked_using_mnemonic(
534540
"sudden tenant fault inject concert weather maid people chunk youth stumble grit",
535541
);
542+
let mut xpub_cache = Bip32XpubCache::new();
536543
let coin_params = super::super::params::get(pb::BtcCoin::Btc);
537544
// p2wpkh
538545
assert_eq!(
539546
Payload::from_simple(
547+
&mut xpub_cache,
540548
coin_params,
541549
SimpleType::P2wpkh,
542550
&[84 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0]
@@ -550,6 +558,7 @@ mod tests {
550558
// p2wpkh-p2sh
551559
assert_eq!(
552560
Payload::from_simple(
561+
&mut xpub_cache,
553562
coin_params,
554563
SimpleType::P2wpkhP2sh,
555564
&[49 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0]
@@ -563,6 +572,7 @@ mod tests {
563572
// p2tr
564573
assert_eq!(
565574
Payload::from_simple(
575+
&mut xpub_cache,
566576
coin_params,
567577
SimpleType::P2tr,
568578
&[86 + HARDENED, 0 + HARDENED, 0 + HARDENED, 0, 0]

0 commit comments

Comments
 (0)