Skip to content

Commit 0334a51

Browse files
committed
api: add BTCXpubsRequest to fetch multiple xpubs at once
To reduce the number of secure chip operations needed, we introduce a new API call to fetch multiple xpubs at once. This only requires two operations in total, instead of two per xpub. We want to reduce the number of secure chip operations to avoid running into Optiga's throttling security measure.
1 parent 1aa1169 commit 0334a51

File tree

14 files changed

+525
-74
lines changed

14 files changed

+525
-74
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
1212
- Remove option to restore from 18 recovery words
1313
- simulator: enable Test Merchant for payment requests
1414
- simulator: simulate a Nova device
15+
- Add API call to fetch multiple xpubs at once
1516

1617
### 9.23.1
1718
- EVM: add HyperEVM (HYPE) and SONIC (S) to known networks

messages/btc.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ message BTCPubRequest {
9393
bool display = 5;
9494
}
9595

96+
message BTCXpubsRequest{
97+
enum XPubType {
98+
UNKNOWN = 0;
99+
XPUB = 1;
100+
TPUB = 2;
101+
}
102+
BTCCoin coin = 1;
103+
XPubType xpub_type = 2;
104+
repeated Keypath keypaths = 3;
105+
}
106+
96107
message BTCScriptConfigWithKeypath {
97108
BTCScriptConfig script_config = 2;
98109
repeated uint32 keypath = 3;
@@ -281,6 +292,7 @@ message BTCRequest {
281292
BTCSignMessageRequest sign_message = 6;
282293
AntiKleptoSignatureRequest antiklepto_signature = 7;
283294
BTCPaymentRequestRequest payment_request = 8;
295+
BTCXpubsRequest xpubs = 9;
284296
}
285297
}
286298

@@ -291,5 +303,6 @@ message BTCResponse {
291303
BTCSignNextResponse sign_next = 3;
292304
BTCSignMessageResponse sign_message = 4;
293305
AntiKleptoSignerCommitment antiklepto_signer_commitment = 5;
306+
PubsResponse pubs = 6;
294307
}
295308
}

messages/common.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ message PubResponse {
1919
string pub = 1;
2020
}
2121

22+
message PubsResponse {
23+
repeated string pubs = 1;
24+
}
25+
2226
message RootFingerprintRequest {
2327
}
2428

py/bitbox02/bitbox02/bitbox02/bitbox02.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,26 @@ def btc_xpub(
320320
)
321321
return self._msg_query(request).pub.pub
322322

323+
def btc_xpubs(
324+
self,
325+
keypaths: Sequence[Sequence[int]],
326+
coin: "btc.BTCCoin.V" = btc.BTC,
327+
xpub_type: "btc.BTCXpubsRequest.XPubType.V" = btc.BTCXpubsRequest.XPUB,
328+
) -> List[str]:
329+
"""
330+
Retrieve up to 20 xpubs.
331+
"""
332+
self._require_atleast(semver.VersionInfo(9, 24, 0))
333+
btc_request = btc.BTCRequest()
334+
btc_request.xpubs.CopyFrom(
335+
btc.BTCXpubsRequest(
336+
coin=coin,
337+
keypaths=[common.Keypath(keypath=keypath) for keypath in keypaths],
338+
xpub_type=xpub_type,
339+
)
340+
)
341+
return list(self._btc_msg_query(btc_request, expected_response="pubs").pubs.pubs)
342+
323343
def btc_address(
324344
self,
325345
keypath: Sequence[int],

py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class DeviceInfoResponse(google.protobuf.message.Message):
7979
firmware_hash: builtins.bytes
8080
"""Hash of the currently active Bluetooth firmware on the device."""
8181
firmware_version: builtins.str
82-
"""Firmware version, formated as "major.minor.patch"."""
82+
"""Firmware version, formated as an unsigned integer "1", "2", etc."""
8383
enabled: builtins.bool
8484
"""True if Bluetooth is enabled"""
8585
def __init__(

py/bitbox02/bitbox02/communication/generated/btc_pb2.py

Lines changed: 57 additions & 53 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,43 @@ class BTCPubRequest(google.protobuf.message.Message):
263263

264264
global___BTCPubRequest = BTCPubRequest
265265

266+
@typing.final
267+
class BTCXpubsRequest(google.protobuf.message.Message):
268+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
269+
270+
class _XPubType:
271+
ValueType = typing.NewType("ValueType", builtins.int)
272+
V: typing_extensions.TypeAlias = ValueType
273+
274+
class _XPubTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCXpubsRequest._XPubType.ValueType], builtins.type):
275+
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
276+
UNKNOWN: BTCXpubsRequest._XPubType.ValueType # 0
277+
XPUB: BTCXpubsRequest._XPubType.ValueType # 1
278+
TPUB: BTCXpubsRequest._XPubType.ValueType # 2
279+
280+
class XPubType(_XPubType, metaclass=_XPubTypeEnumTypeWrapper): ...
281+
UNKNOWN: BTCXpubsRequest.XPubType.ValueType # 0
282+
XPUB: BTCXpubsRequest.XPubType.ValueType # 1
283+
TPUB: BTCXpubsRequest.XPubType.ValueType # 2
284+
285+
COIN_FIELD_NUMBER: builtins.int
286+
XPUB_TYPE_FIELD_NUMBER: builtins.int
287+
KEYPATHS_FIELD_NUMBER: builtins.int
288+
coin: global___BTCCoin.ValueType
289+
xpub_type: global___BTCXpubsRequest.XPubType.ValueType
290+
@property
291+
def keypaths(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[common_pb2.Keypath]: ...
292+
def __init__(
293+
self,
294+
*,
295+
coin: global___BTCCoin.ValueType = ...,
296+
xpub_type: global___BTCXpubsRequest.XPubType.ValueType = ...,
297+
keypaths: collections.abc.Iterable[common_pb2.Keypath] | None = ...,
298+
) -> None: ...
299+
def ClearField(self, field_name: typing.Literal["coin", b"coin", "keypaths", b"keypaths", "xpub_type", b"xpub_type"]) -> None: ...
300+
301+
global___BTCXpubsRequest = BTCXpubsRequest
302+
266303
@typing.final
267304
class BTCScriptConfigWithKeypath(google.protobuf.message.Message):
268305
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -826,6 +863,7 @@ class BTCRequest(google.protobuf.message.Message):
826863
SIGN_MESSAGE_FIELD_NUMBER: builtins.int
827864
ANTIKLEPTO_SIGNATURE_FIELD_NUMBER: builtins.int
828865
PAYMENT_REQUEST_FIELD_NUMBER: builtins.int
866+
XPUBS_FIELD_NUMBER: builtins.int
829867
@property
830868
def is_script_config_registered(self) -> global___BTCIsScriptConfigRegisteredRequest: ...
831869
@property
@@ -842,6 +880,8 @@ class BTCRequest(google.protobuf.message.Message):
842880
def antiklepto_signature(self) -> antiklepto_pb2.AntiKleptoSignatureRequest: ...
843881
@property
844882
def payment_request(self) -> global___BTCPaymentRequestRequest: ...
883+
@property
884+
def xpubs(self) -> global___BTCXpubsRequest: ...
845885
def __init__(
846886
self,
847887
*,
@@ -853,10 +893,11 @@ class BTCRequest(google.protobuf.message.Message):
853893
sign_message: global___BTCSignMessageRequest | None = ...,
854894
antiklepto_signature: antiklepto_pb2.AntiKleptoSignatureRequest | None = ...,
855895
payment_request: global___BTCPaymentRequestRequest | None = ...,
896+
xpubs: global___BTCXpubsRequest | None = ...,
856897
) -> None: ...
857-
def HasField(self, field_name: typing.Literal["antiklepto_signature", b"antiklepto_signature", "is_script_config_registered", b"is_script_config_registered", "payment_request", b"payment_request", "prevtx_init", b"prevtx_init", "prevtx_input", b"prevtx_input", "prevtx_output", b"prevtx_output", "register_script_config", b"register_script_config", "request", b"request", "sign_message", b"sign_message"]) -> builtins.bool: ...
858-
def ClearField(self, field_name: typing.Literal["antiklepto_signature", b"antiklepto_signature", "is_script_config_registered", b"is_script_config_registered", "payment_request", b"payment_request", "prevtx_init", b"prevtx_init", "prevtx_input", b"prevtx_input", "prevtx_output", b"prevtx_output", "register_script_config", b"register_script_config", "request", b"request", "sign_message", b"sign_message"]) -> None: ...
859-
def WhichOneof(self, oneof_group: typing.Literal["request", b"request"]) -> typing.Literal["is_script_config_registered", "register_script_config", "prevtx_init", "prevtx_input", "prevtx_output", "sign_message", "antiklepto_signature", "payment_request"] | None: ...
898+
def HasField(self, field_name: typing.Literal["antiklepto_signature", b"antiklepto_signature", "is_script_config_registered", b"is_script_config_registered", "payment_request", b"payment_request", "prevtx_init", b"prevtx_init", "prevtx_input", b"prevtx_input", "prevtx_output", b"prevtx_output", "register_script_config", b"register_script_config", "request", b"request", "sign_message", b"sign_message", "xpubs", b"xpubs"]) -> builtins.bool: ...
899+
def ClearField(self, field_name: typing.Literal["antiklepto_signature", b"antiklepto_signature", "is_script_config_registered", b"is_script_config_registered", "payment_request", b"payment_request", "prevtx_init", b"prevtx_init", "prevtx_input", b"prevtx_input", "prevtx_output", b"prevtx_output", "register_script_config", b"register_script_config", "request", b"request", "sign_message", b"sign_message", "xpubs", b"xpubs"]) -> None: ...
900+
def WhichOneof(self, oneof_group: typing.Literal["request", b"request"]) -> typing.Literal["is_script_config_registered", "register_script_config", "prevtx_init", "prevtx_input", "prevtx_output", "sign_message", "antiklepto_signature", "payment_request", "xpubs"] | None: ...
860901

861902
global___BTCRequest = BTCRequest
862903

@@ -869,6 +910,7 @@ class BTCResponse(google.protobuf.message.Message):
869910
SIGN_NEXT_FIELD_NUMBER: builtins.int
870911
SIGN_MESSAGE_FIELD_NUMBER: builtins.int
871912
ANTIKLEPTO_SIGNER_COMMITMENT_FIELD_NUMBER: builtins.int
913+
PUBS_FIELD_NUMBER: builtins.int
872914
@property
873915
def success(self) -> global___BTCSuccess: ...
874916
@property
@@ -879,6 +921,8 @@ class BTCResponse(google.protobuf.message.Message):
879921
def sign_message(self) -> global___BTCSignMessageResponse: ...
880922
@property
881923
def antiklepto_signer_commitment(self) -> antiklepto_pb2.AntiKleptoSignerCommitment: ...
924+
@property
925+
def pubs(self) -> common_pb2.PubsResponse: ...
882926
def __init__(
883927
self,
884928
*,
@@ -887,9 +931,10 @@ class BTCResponse(google.protobuf.message.Message):
887931
sign_next: global___BTCSignNextResponse | None = ...,
888932
sign_message: global___BTCSignMessageResponse | None = ...,
889933
antiklepto_signer_commitment: antiklepto_pb2.AntiKleptoSignerCommitment | None = ...,
934+
pubs: common_pb2.PubsResponse | None = ...,
890935
) -> None: ...
891-
def HasField(self, field_name: typing.Literal["antiklepto_signer_commitment", b"antiklepto_signer_commitment", "is_script_config_registered", b"is_script_config_registered", "response", b"response", "sign_message", b"sign_message", "sign_next", b"sign_next", "success", b"success"]) -> builtins.bool: ...
892-
def ClearField(self, field_name: typing.Literal["antiklepto_signer_commitment", b"antiklepto_signer_commitment", "is_script_config_registered", b"is_script_config_registered", "response", b"response", "sign_message", b"sign_message", "sign_next", b"sign_next", "success", b"success"]) -> None: ...
893-
def WhichOneof(self, oneof_group: typing.Literal["response", b"response"]) -> typing.Literal["success", "is_script_config_registered", "sign_next", "sign_message", "antiklepto_signer_commitment"] | None: ...
936+
def HasField(self, field_name: typing.Literal["antiklepto_signer_commitment", b"antiklepto_signer_commitment", "is_script_config_registered", b"is_script_config_registered", "pubs", b"pubs", "response", b"response", "sign_message", b"sign_message", "sign_next", b"sign_next", "success", b"success"]) -> builtins.bool: ...
937+
def ClearField(self, field_name: typing.Literal["antiklepto_signer_commitment", b"antiklepto_signer_commitment", "is_script_config_registered", b"is_script_config_registered", "pubs", b"pubs", "response", b"response", "sign_message", b"sign_message", "sign_next", b"sign_next", "success", b"success"]) -> None: ...
938+
def WhichOneof(self, oneof_group: typing.Literal["response", b"response"]) -> typing.Literal["success", "is_script_config_registered", "sign_next", "sign_message", "antiklepto_signer_commitment", "pubs"] | None: ...
894939

895940
global___BTCResponse = BTCResponse

py/bitbox02/bitbox02/communication/generated/common_pb2.py

Lines changed: 13 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

py/bitbox02/bitbox02/communication/generated/common_pb2.pyi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ class PubResponse(google.protobuf.message.Message):
4040

4141
global___PubResponse = PubResponse
4242

43+
@typing.final
44+
class PubsResponse(google.protobuf.message.Message):
45+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
46+
47+
PUBS_FIELD_NUMBER: builtins.int
48+
@property
49+
def pubs(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ...
50+
def __init__(
51+
self,
52+
*,
53+
pubs: collections.abc.Iterable[builtins.str] | None = ...,
54+
) -> None: ...
55+
def ClearField(self, field_name: typing.Literal["pubs", b"pubs"]) -> None: ...
56+
57+
global___PubsResponse = PubsResponse
58+
4359
@typing.final
4460
class RootFingerprintRequest(google.protobuf.message.Message):
4561
DESCRIPTOR: google.protobuf.descriptor.Descriptor

py/send_message.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,14 @@ def _display_zpub(self) -> None:
331331
except UserAbortException:
332332
eprint("Aborted by user")
333333

334+
def _btc_xpubs(self) -> None:
335+
xpubs = self._device.btc_xpubs(
336+
keypaths=[[84 + HARDENED, 0 + HARDENED, i + HARDENED] for i in range(20)],
337+
)
338+
print("xpubs for m/84'/0'/{0'..19'}:")
339+
for xpub in xpubs:
340+
print(xpub)
341+
334342
def _get_electrum_encryption_key(self) -> None:
335343
print(
336344
"Electrum wallet encryption xpub at keypath m/4541509'/1112098098':",
@@ -376,7 +384,7 @@ def _btc_multisig_config(self, coin: "bitbox02.btc.BTCCoin.V") -> bitbox02.btc.B
376384
my_xpub = self._device.btc_xpub(
377385
keypath=account_keypath,
378386
coin=coin,
379-
xpub_type=bitbox02.btc.BTCPubRequest.XPUB, # pylint: disable=no-member,
387+
xpub_type=bitbox02.btc.BTCPubRequest.XPUB,
380388
display=False,
381389
)
382390
multisig_config = bitbox02.btc.BTCScriptConfig(
@@ -1503,6 +1511,7 @@ def _menu_init(self) -> None:
15031511
("Change device name", self._change_name_workflow),
15041512
("Get root fingerprint", self._get_root_fingerprint),
15051513
("Retrieve zpub of first account", self._display_zpub),
1514+
("Retrieve multiple xpubs", self._btc_xpubs),
15061515
("Retrieve a BTC address", self._btc_address),
15071516
("Retrieve a BTC Multisig address", self._btc_multisig_address),
15081517
("Retrieve a BTC policy address", self._btc_policy_address),

0 commit comments

Comments
 (0)