api: add BTCXpubsRequest to fetch multiple xpubs at once
What changed, and why it matters
This commit adds a new device API that lets a host computer request up to 20 Bitcoin extended public keys (xpubs) in a single call, instead of making one call per key. The goal is to reduce how often the device talks to its secure chip, avoiding a throttling safeguard. The change itself is a feature addition, not a fix for a known vulnerability, and the code applies existing keypath validation and a double-check against bitflips.
Review the new xpubs handler for consistency with existing xpub export security policy, especially whether batch export should require user confirmation or rate-limiting. Verify that validate_xpub() correctly covers all accepted keypaths and that the 20-item limit is enforced before any secure-chip operation. No immediate patch is indicated by the diff alone.
Security signals we found
New API surface added for batch xpub retrieval
Existing keypath validation reused (validate_xpub)
Double-read of root xprv and double-derivation to mitigate bitflips/faults
Hard limit of 20 xpubs per request to bound resource use
No user confirmation required for xpub export (same as existing single-xpub API)
Commit explicitly states motivation is avoiding Optiga throttling, not a security bugfix
Evidence from the diff
The patch introduces BTCXpubsRequest/PubsResponse protobuf messages, a Rust handler in src/rust/bitbox02-rust/src/hww/api/bitcoin/xpubs.rs, and a keystore helper get_xpubs_twice(). The handler limits requests to 20 keypaths, validates each keypath with the existing validate_xpub() rules, maps the new xpub_type enum to the existing one, and returns serialized xpub strings. get_xpubs_twice() fetches the root private key twice from the secure chip to detect bitflips, then derives each requested xpub twice in software to detect derivation errors. Unit tests verify the 20-keypath limit, invalid keypath rejection, and that only two secure-chip events occur for a multi-xpub request.
Changed components
BitBox02 firmware Bitcoin APImessages/btc.protomessages/common.protopy/bitbox02 Python client librarysrc/rust/bitbox02-rust/src/hww/api/bitcoin/xpubs.rssrc/rust/bitbox02-rust/src/keystore.rsInspect captured patch +525 / −74
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c524124..31954aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
- Remove option to restore from 18 recovery words
- simulator: enable Test Merchant for payment requests
- simulator: simulate a Nova device
+- Add API call to fetch multiple xpubs at once
### 9.23.1
- EVM: add HyperEVM (HYPE) and SONIC (S) to known networks
diff --git a/messages/btc.proto b/messages/btc.proto
index 7a49fe4..24669ff 100644
--- a/messages/btc.proto
+++ b/messages/btc.proto
@@ -93,6 +93,17 @@ message BTCPubRequest {
bool display = 5;
}
+message BTCXpubsRequest{
+ enum XPubType {
+ UNKNOWN = 0;
+ XPUB = 1;
+ TPUB = 2;
+ }
+ BTCCoin coin = 1;
+ XPubType xpub_type = 2;
+ repeated Keypath keypaths = 3;
+}
+
message BTCScriptConfigWithKeypath {
BTCScriptConfig script_config = 2;
repeated uint32 keypath = 3;
@@ -281,6 +292,7 @@ message BTCRequest {
BTCSignMessageRequest sign_message = 6;
AntiKleptoSignatureRequest antiklepto_signature = 7;
BTCPaymentRequestRequest payment_request = 8;
+ BTCXpubsRequest xpubs = 9;
}
}
@@ -291,5 +303,6 @@ message BTCResponse {
BTCSignNextResponse sign_next = 3;
BTCSignMessageResponse sign_message = 4;
AntiKleptoSignerCommitment antiklepto_signer_commitment = 5;
+ PubsResponse pubs = 6;
}
}
diff --git a/messages/common.proto b/messages/common.proto
index 0b80c6f..0fed08c 100644
--- a/messages/common.proto
+++ b/messages/common.proto
@@ -19,6 +19,10 @@ message PubResponse {
string pub = 1;
}
+message PubsResponse {
+ repeated string pubs = 1;
+}
+
message RootFingerprintRequest {
}
diff --git a/py/bitbox02/bitbox02/bitbox02/bitbox02.py b/py/bitbox02/bitbox02/bitbox02/bitbox02.py
index a25d785..753cde5 100644
--- a/py/bitbox02/bitbox02/bitbox02/bitbox02.py
+++ b/py/bitbox02/bitbox02/bitbox02/bitbox02.py
@@ -320,6 +320,26 @@ class BitBox02(BitBoxCommonAPI):
)
return self._msg_query(request).pub.pub
+ def btc_xpubs(
+ self,
+ keypaths: Sequence[Sequence[int]],
+ coin: "btc.BTCCoin.V" = btc.BTC,
+ xpub_type: "btc.BTCXpubsRequest.XPubType.V" = btc.BTCXpubsRequest.XPUB,
+ ) -> List[str]:
+ """
+ Retrieve up to 20 xpubs.
+ """
+ self._require_atleast(semver.VersionInfo(9, 24, 0))
+ btc_request = btc.BTCRequest()
+ btc_request.xpubs.CopyFrom(
+ btc.BTCXpubsRequest(
+ coin=coin,
+ keypaths=[common.Keypath(keypath=keypath) for keypath in keypaths],
+ xpub_type=xpub_type,
+ )
+ )
+ return list(self._btc_msg_query(btc_request, expected_response="pubs").pubs.pubs)
+
def btc_address(
self,
keypath: Sequence[int],
diff --git a/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi
index 8591bfe..b928e85 100644
--- a/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi
+++ b/py/bitbox02/bitbox02/communication/generated/bitbox02_system_pb2.pyi
@@ -79,7 +79,7 @@ class DeviceInfoResponse(google.protobuf.message.Message):
firmware_hash: builtins.bytes
"""Hash of the currently active Bluetooth firmware on the device."""
firmware_version: builtins.str
- """Firmware version, formated as "major.minor.patch"."""
+ """Firmware version, formated as an unsigned integer "1", "2", etc."""
enabled: builtins.bool
"""True if Bluetooth is enabled"""
def __init__(
diff --git a/py/bitbox02/bitbox02/communication/generated/btc_pb2.py b/py/bitbox02/bitbox02/communication/generated/btc_pb2.py
index 111d577..45148fd 100644
--- a/py/bitbox02/bitbox02/communication/generated/btc_pb2.py
+++ b/py/bitbox02/bitbox02/communication/generated/btc_pb2.py
@@ -15,17 +15,17 @@ from . import common_pb2 as common__pb2
from . import antiklepto_pb2 as antiklepto__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tbtc.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xc6\x04\n\x0f\x42TCScriptConfig\x12G\n\x0bsimple_type\x18\x01 \x01(\x0e\x32\x30.shiftcrypto.bitbox02.BTCScriptConfig.SimpleTypeH\x00\x12\x42\n\x08multisig\x18\x02 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCScriptConfig.MultisigH\x00\x12>\n\x06policy\x18\x03 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCScriptConfig.PolicyH\x00\x1a\xd9\x01\n\x08Multisig\x12\x11\n\tthreshold\x18\x01 \x01(\r\x12)\n\x05xpubs\x18\x02 \x03(\x0b\x32\x1a.shiftcrypto.bitbox02.XPub\x12\x16\n\x0eour_xpub_index\x18\x03 \x01(\r\x12N\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.BTCScriptConfig.Multisig.ScriptType\"\'\n\nScriptType\x12\t\n\x05P2WSH\x10\x00\x12\x0e\n\nP2WSH_P2SH\x10\x01\x1aK\n\x06Policy\x12\x0e\n\x06policy\x18\x01 \x01(\t\x12\x31\n\x04keys\x18\x02 \x03(\x0b\x32#.shiftcrypto.bitbox02.KeyOriginInfo\"3\n\nSimpleType\x12\x0f\n\x0bP2WPKH_P2SH\x10\x00\x12\n\n\x06P2WPKH\x10\x01\x12\x08\n\x04P2TR\x10\x02\x42\x08\n\x06\x63onfig\"\xfc\x02\n\rBTCPubRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x41\n\txpub_type\x18\x03 \x01(\x0e\x32,.shiftcrypto.bitbox02.BTCPubRequest.XPubTypeH\x00\x12>\n\rscript_config\x18\x04 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfigH\x00\x12\x0f\n\x07\x64isplay\x18\x05 \x01(\x08\"\x8e\x01\n\x08XPubType\x12\x08\n\x04TPUB\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04YPUB\x10\x02\x12\x08\n\x04ZPUB\x10\x03\x12\x08\n\x04VPUB\x10\x04\x12\x08\n\x04UPUB\x10\x05\x12\x10\n\x0c\x43\x41PITAL_VPUB\x10\x06\x12\x10\n\x0c\x43\x41PITAL_ZPUB\x10\x07\x12\x10\n\x0c\x43\x41PITAL_UPUB\x10\x08\x12\x10\n\x0c\x43\x41PITAL_YPUB\x10\tB\x08\n\x06output\"k\n\x1a\x42TCScriptConfigWithKeypath\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\xbf\x03\n\x12\x42TCSignInitRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12H\n\x0escript_configs\x18\x02 \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0f\n\x07version\x18\x04 \x01(\r\x12\x12\n\nnum_inputs\x18\x05 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x06 \x01(\r\x12\x10\n\x08locktime\x18\x07 \x01(\r\x12H\n\x0b\x66ormat_unit\x18\x08 \x01(\x0e\x32\x33.shiftcrypto.bitbox02.BTCSignInitRequest.FormatUnit\x12\'\n\x1f\x63ontains_silent_payment_outputs\x18\t \x01(\x08\x12O\n\x15output_script_configs\x18\n \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\"\"\n\nFormatUnit\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03SAT\x10\x01\"\xc4\x03\n\x13\x42TCSignNextResponse\x12<\n\x04type\x18\x01 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCSignNextResponse.Type\x12\r\n\x05index\x18\x02 \x01(\r\x12\x15\n\rhas_signature\x18\x03 \x01(\x08\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x12\n\nprev_index\x18\x05 \x01(\r\x12W\n\x1d\x61nti_klepto_signer_commitment\x18\x06 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitment\x12!\n\x19generated_output_pkscript\x18\x07 \x01(\x0c\x12!\n\x19silent_payment_dleq_proof\x18\x08 \x01(\x0c\"\x82\x01\n\x04Type\x12\t\n\x05INPUT\x10\x00\x12\n\n\x06OUTPUT\x10\x01\x12\x08\n\x04\x44ONE\x10\x02\x12\x0f\n\x0bPREVTX_INIT\x10\x03\x12\x10\n\x0cPREVTX_INPUT\x10\x04\x12\x11\n\rPREVTX_OUTPUT\x10\x05\x12\x0e\n\nHOST_NONCE\x10\x06\x12\x13\n\x0fPAYMENT_REQUEST\x10\x07\"\xea\x01\n\x13\x42TCSignInputRequest\x12\x13\n\x0bprevOutHash\x18\x01 \x01(\x0c\x12\x14\n\x0cprevOutIndex\x18\x02 \x01(\r\x12\x14\n\x0cprevOutValue\x18\x03 \x01(\x04\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x0f\n\x07keypath\x18\x06 \x03(\r\x12\x1b\n\x13script_config_index\x18\x07 \x01(\r\x12R\n\x15host_nonce_commitment\x18\x08 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"\x9f\x03\n\x14\x42TCSignOutputRequest\x12\x0c\n\x04ours\x18\x01 \x01(\x08\x12\x31\n\x04type\x18\x02 \x01(\x0e\x32#.shiftcrypto.bitbox02.BTCOutputType\x12\r\n\x05value\x18\x03 \x01(\x04\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x0f\n\x07keypath\x18\x05 \x03(\r\x12\x1b\n\x13script_config_index\x18\x06 \x01(\r\x12\"\n\x15payment_request_index\x18\x07 \x01(\rH\x00\x88\x01\x01\x12P\n\x0esilent_payment\x18\x08 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCSignOutputRequest.SilentPayment\x12\'\n\x1aoutput_script_config_index\x18\t \x01(\rH\x01\x88\x01\x01\x1a \n\rSilentPayment\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x18\n\x16_payment_request_indexB\x1d\n\x1b_output_script_config_index\"\x99\x01\n\x1b\x42TCScriptConfigRegistration\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\x0c\n\nBTCSuccess\"m\n\"BTCIsScriptConfigRegisteredRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\"<\n#BTCIsScriptConfigRegisteredResponse\x12\x15\n\ris_registered\x18\x01 \x01(\x08\"\xfc\x01\n\x1e\x42TCRegisterScriptConfigRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\x12\x0c\n\x04name\x18\x02 \x01(\t\x12P\n\txpub_type\x18\x03 \x01(\x0e\x32=.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequest.XPubType\"1\n\x08XPubType\x12\x11\n\rAUTO_ELECTRUM\x10\x00\x12\x12\n\x0e\x41UTO_XPUB_TPUB\x10\x01\"b\n\x14\x42TCPrevTxInitRequest\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x12\n\nnum_inputs\x18\x02 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x03 \x01(\r\x12\x10\n\x08locktime\x18\x04 \x01(\r\"r\n\x15\x42TCPrevTxInputRequest\x12\x15\n\rprev_out_hash\x18\x01 \x01(\x0c\x12\x16\n\x0eprev_out_index\x18\x02 \x01(\r\x12\x18\n\x10signature_script\x18\x03 \x01(\x0c\x12\x10\n\x08sequence\x18\x04 \x01(\r\">\n\x16\x42TCPrevTxOutputRequest\x12\r\n\x05value\x18\x01 \x01(\x04\x12\x15\n\rpubkey_script\x18\x02 \x01(\x0c\"\xab\x02\n\x18\x42TCPaymentRequestRequest\x12\x16\n\x0erecipient_name\x18\x01 \x01(\t\x12\x42\n\x05memos\x18\x02 \x03(\x0b\x32\x33.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x14\n\x0ctotal_amount\x18\x04 \x01(\x04\x12\x11\n\tsignature\x18\x05 \x01(\x0c\x1a{\n\x04Memo\x12Q\n\ttext_memo\x18\x01 \x01(\x0b\x32<.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.TextMemoH\x00\x1a\x18\n\x08TextMemo\x12\x0c\n\x04note\x18\x01 \x01(\tB\x06\n\x04memo\"\xee\x01\n\x15\x42TCSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12G\n\rscript_config\x18\x02 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"+\n\x16\x42TCSignMessageResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\x81\x05\n\nBTCRequest\x12_\n\x1bis_script_config_registered\x18\x01 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredRequestH\x00\x12V\n\x16register_script_config\x18\x02 \x01(\x0b\x32\x34.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequestH\x00\x12\x41\n\x0bprevtx_init\x18\x03 \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCPrevTxInitRequestH\x00\x12\x43\n\x0cprevtx_input\x18\x04 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCPrevTxInputRequestH\x00\x12\x45\n\rprevtx_output\x18\x05 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCPrevTxOutputRequestH\x00\x12\x43\n\x0csign_message\x18\x06 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x07 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12I\n\x0fpayment_request\x18\x08 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCPaymentRequestRequestH\x00\x42\t\n\x07request\"\x90\x03\n\x0b\x42TCResponse\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCSuccessH\x00\x12`\n\x1bis_script_config_registered\x18\x02 \x01(\x0b\x32\x39.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredResponseH\x00\x12>\n\tsign_next\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x44\n\x0csign_message\x18\x04 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCSignMessageResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x42\n\n\x08response*9\n\x07\x42TCCoin\x12\x07\n\x03\x42TC\x10\x00\x12\x08\n\x04TBTC\x10\x01\x12\x07\n\x03LTC\x10\x02\x12\x08\n\x04TLTC\x10\x03\x12\x08\n\x04RBTC\x10\x04*R\n\rBTCOutputType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05P2PKH\x10\x01\x12\x08\n\x04P2SH\x10\x02\x12\n\n\x06P2WPKH\x10\x03\x12\t\n\x05P2WSH\x10\x04\x12\x08\n\x04P2TR\x10\x05\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tbtc.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xc6\x04\n\x0f\x42TCScriptConfig\x12G\n\x0bsimple_type\x18\x01 \x01(\x0e\x32\x30.shiftcrypto.bitbox02.BTCScriptConfig.SimpleTypeH\x00\x12\x42\n\x08multisig\x18\x02 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCScriptConfig.MultisigH\x00\x12>\n\x06policy\x18\x03 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCScriptConfig.PolicyH\x00\x1a\xd9\x01\n\x08Multisig\x12\x11\n\tthreshold\x18\x01 \x01(\r\x12)\n\x05xpubs\x18\x02 \x03(\x0b\x32\x1a.shiftcrypto.bitbox02.XPub\x12\x16\n\x0eour_xpub_index\x18\x03 \x01(\r\x12N\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.BTCScriptConfig.Multisig.ScriptType\"\'\n\nScriptType\x12\t\n\x05P2WSH\x10\x00\x12\x0e\n\nP2WSH_P2SH\x10\x01\x1aK\n\x06Policy\x12\x0e\n\x06policy\x18\x01 \x01(\t\x12\x31\n\x04keys\x18\x02 \x03(\x0b\x32#.shiftcrypto.bitbox02.KeyOriginInfo\"3\n\nSimpleType\x12\x0f\n\x0bP2WPKH_P2SH\x10\x00\x12\n\n\x06P2WPKH\x10\x01\x12\x08\n\x04P2TR\x10\x02\x42\x08\n\x06\x63onfig\"\xfc\x02\n\rBTCPubRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x41\n\txpub_type\x18\x03 \x01(\x0e\x32,.shiftcrypto.bitbox02.BTCPubRequest.XPubTypeH\x00\x12>\n\rscript_config\x18\x04 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfigH\x00\x12\x0f\n\x07\x64isplay\x18\x05 \x01(\x08\"\x8e\x01\n\x08XPubType\x12\x08\n\x04TPUB\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04YPUB\x10\x02\x12\x08\n\x04ZPUB\x10\x03\x12\x08\n\x04VPUB\x10\x04\x12\x08\n\x04UPUB\x10\x05\x12\x10\n\x0c\x43\x41PITAL_VPUB\x10\x06\x12\x10\n\x0c\x43\x41PITAL_ZPUB\x10\x07\x12\x10\n\x0c\x43\x41PITAL_UPUB\x10\x08\x12\x10\n\x0c\x43\x41PITAL_YPUB\x10\tB\x08\n\x06output\"\xdf\x01\n\x0f\x42TCXpubsRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x41\n\txpub_type\x18\x02 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCXpubsRequest.XPubType\x12/\n\x08keypaths\x18\x03 \x03(\x0b\x32\x1d.shiftcrypto.bitbox02.Keypath\"+\n\x08XPubType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04TPUB\x10\x02\"k\n\x1a\x42TCScriptConfigWithKeypath\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\xbf\x03\n\x12\x42TCSignInitRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12H\n\x0escript_configs\x18\x02 \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0f\n\x07version\x18\x04 \x01(\r\x12\x12\n\nnum_inputs\x18\x05 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x06 \x01(\r\x12\x10\n\x08locktime\x18\x07 \x01(\r\x12H\n\x0b\x66ormat_unit\x18\x08 \x01(\x0e\x32\x33.shiftcrypto.bitbox02.BTCSignInitRequest.FormatUnit\x12\'\n\x1f\x63ontains_silent_payment_outputs\x18\t \x01(\x08\x12O\n\x15output_script_configs\x18\n \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\"\"\n\nFormatUnit\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03SAT\x10\x01\"\xc4\x03\n\x13\x42TCSignNextResponse\x12<\n\x04type\x18\x01 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCSignNextResponse.Type\x12\r\n\x05index\x18\x02 \x01(\r\x12\x15\n\rhas_signature\x18\x03 \x01(\x08\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x12\n\nprev_index\x18\x05 \x01(\r\x12W\n\x1d\x61nti_klepto_signer_commitment\x18\x06 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitment\x12!\n\x19generated_output_pkscript\x18\x07 \x01(\x0c\x12!\n\x19silent_payment_dleq_proof\x18\x08 \x01(\x0c\"\x82\x01\n\x04Type\x12\t\n\x05INPUT\x10\x00\x12\n\n\x06OUTPUT\x10\x01\x12\x08\n\x04\x44ONE\x10\x02\x12\x0f\n\x0bPREVTX_INIT\x10\x03\x12\x10\n\x0cPREVTX_INPUT\x10\x04\x12\x11\n\rPREVTX_OUTPUT\x10\x05\x12\x0e\n\nHOST_NONCE\x10\x06\x12\x13\n\x0fPAYMENT_REQUEST\x10\x07\"\xea\x01\n\x13\x42TCSignInputRequest\x12\x13\n\x0bprevOutHash\x18\x01 \x01(\x0c\x12\x14\n\x0cprevOutIndex\x18\x02 \x01(\r\x12\x14\n\x0cprevOutValue\x18\x03 \x01(\x04\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x0f\n\x07keypath\x18\x06 \x03(\r\x12\x1b\n\x13script_config_index\x18\x07 \x01(\r\x12R\n\x15host_nonce_commitment\x18\x08 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"\x9f\x03\n\x14\x42TCSignOutputRequest\x12\x0c\n\x04ours\x18\x01 \x01(\x08\x12\x31\n\x04type\x18\x02 \x01(\x0e\x32#.shiftcrypto.bitbox02.BTCOutputType\x12\r\n\x05value\x18\x03 \x01(\x04\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x0f\n\x07keypath\x18\x05 \x03(\r\x12\x1b\n\x13script_config_index\x18\x06 \x01(\r\x12\"\n\x15payment_request_index\x18\x07 \x01(\rH\x00\x88\x01\x01\x12P\n\x0esilent_payment\x18\x08 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCSignOutputRequest.SilentPayment\x12\'\n\x1aoutput_script_config_index\x18\t \x01(\rH\x01\x88\x01\x01\x1a \n\rSilentPayment\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\tB\x18\n\x16_payment_request_indexB\x1d\n\x1b_output_script_config_index\"\x99\x01\n\x1b\x42TCScriptConfigRegistration\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\x0c\n\nBTCSuccess\"m\n\"BTCIsScriptConfigRegisteredRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\"<\n#BTCIsScriptConfigRegisteredResponse\x12\x15\n\ris_registered\x18\x01 \x01(\x08\"\xfc\x01\n\x1e\x42TCRegisterScriptConfigRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\x12\x0c\n\x04name\x18\x02 \x01(\t\x12P\n\txpub_type\x18\x03 \x01(\x0e\x32=.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequest.XPubType\"1\n\x08XPubType\x12\x11\n\rAUTO_ELECTRUM\x10\x00\x12\x12\n\x0e\x41UTO_XPUB_TPUB\x10\x01\"b\n\x14\x42TCPrevTxInitRequest\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x12\n\nnum_inputs\x18\x02 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x03 \x01(\r\x12\x10\n\x08locktime\x18\x04 \x01(\r\"r\n\x15\x42TCPrevTxInputRequest\x12\x15\n\rprev_out_hash\x18\x01 \x01(\x0c\x12\x16\n\x0eprev_out_index\x18\x02 \x01(\r\x12\x18\n\x10signature_script\x18\x03 \x01(\x0c\x12\x10\n\x08sequence\x18\x04 \x01(\r\">\n\x16\x42TCPrevTxOutputRequest\x12\r\n\x05value\x18\x01 \x01(\x04\x12\x15\n\rpubkey_script\x18\x02 \x01(\x0c\"\xab\x02\n\x18\x42TCPaymentRequestRequest\x12\x16\n\x0erecipient_name\x18\x01 \x01(\t\x12\x42\n\x05memos\x18\x02 \x03(\x0b\x32\x33.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x14\n\x0ctotal_amount\x18\x04 \x01(\x04\x12\x11\n\tsignature\x18\x05 \x01(\x0c\x1a{\n\x04Memo\x12Q\n\ttext_memo\x18\x01 \x01(\x0b\x32<.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.TextMemoH\x00\x1a\x18\n\x08TextMemo\x12\x0c\n\x04note\x18\x01 \x01(\tB\x06\n\x04memo\"\xee\x01\n\x15\x42TCSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12G\n\rscript_config\x18\x02 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"+\n\x16\x42TCSignMessageResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xb9\x05\n\nBTCRequest\x12_\n\x1bis_script_config_registered\x18\x01 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredRequestH\x00\x12V\n\x16register_script_config\x18\x02 \x01(\x0b\x32\x34.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequestH\x00\x12\x41\n\x0bprevtx_init\x18\x03 \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCPrevTxInitRequestH\x00\x12\x43\n\x0cprevtx_input\x18\x04 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCPrevTxInputRequestH\x00\x12\x45\n\rprevtx_output\x18\x05 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCPrevTxOutputRequestH\x00\x12\x43\n\x0csign_message\x18\x06 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x07 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12I\n\x0fpayment_request\x18\x08 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCPaymentRequestRequestH\x00\x12\x36\n\x05xpubs\x18\t \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCXpubsRequestH\x00\x42\t\n\x07request\"\xc4\x03\n\x0b\x42TCResponse\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCSuccessH\x00\x12`\n\x1bis_script_config_registered\x18\x02 \x01(\x0b\x32\x39.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredResponseH\x00\x12>\n\tsign_next\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x44\n\x0csign_message\x18\x04 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCSignMessageResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x12\x32\n\x04pubs\x18\x06 \x01(\x0b\x32\".shiftcrypto.bitbox02.PubsResponseH\x00\x42\n\n\x08response*9\n\x07\x42TCCoin\x12\x07\n\x03\x42TC\x10\x00\x12\x08\n\x04TBTC\x10\x01\x12\x07\n\x03LTC\x10\x02\x12\x08\n\x04TLTC\x10\x03\x12\x08\n\x04RBTC\x10\x04*R\n\rBTCOutputType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05P2PKH\x10\x01\x12\x08\n\x04P2SH\x10\x02\x12\n\n\x06P2WPKH\x10\x03\x12\t\n\x05P2WSH\x10\x04\x12\x08\n\x04P2TR\x10\x05\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
- _BTCCOIN._serialized_start=5217
- _BTCCOIN._serialized_end=5274
- _BTCOUTPUTTYPE._serialized_start=5276
- _BTCOUTPUTTYPE._serialized_end=5358
+ _BTCCOIN._serialized_start=5551
+ _BTCCOIN._serialized_end=5608
+ _BTCOUTPUTTYPE._serialized_start=5610
+ _BTCOUTPUTTYPE._serialized_end=5692
_BTCSCRIPTCONFIG._serialized_start=68
_BTCSCRIPTCONFIG._serialized_end=650
_BTCSCRIPTCONFIG_MULTISIG._serialized_start=293
@@ -40,52 +40,56 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_BTCPUBREQUEST._serialized_end=1033
_BTCPUBREQUEST_XPUBTYPE._serialized_start=881
_BTCPUBREQUEST_XPUBTYPE._serialized_end=1023
- _BTCSCRIPTCONFIGWITHKEYPATH._serialized_start=1035
- _BTCSCRIPTCONFIGWITHKEYPATH._serialized_end=1142
- _BTCSIGNINITREQUEST._serialized_start=1145
- _BTCSIGNINITREQUEST._serialized_end=1592
- _BTCSIGNINITREQUEST_FORMATUNIT._serialized_start=1558
- _BTCSIGNINITREQUEST_FORMATUNIT._serialized_end=1592
- _BTCSIGNNEXTRESPONSE._serialized_start=1595
- _BTCSIGNNEXTRESPONSE._serialized_end=2047
- _BTCSIGNNEXTRESPONSE_TYPE._serialized_start=1917
- _BTCSIGNNEXTRESPONSE_TYPE._serialized_end=2047
- _BTCSIGNINPUTREQUEST._serialized_start=2050
- _BTCSIGNINPUTREQUEST._serialized_end=2284
- _BTCSIGNOUTPUTREQUEST._serialized_start=2287
- _BTCSIGNOUTPUTREQUEST._serialized_end=2702
- _BTCSIGNOUTPUTREQUEST_SILENTPAYMENT._serialized_start=2613
- _BTCSIGNOUTPUTREQUEST_SILENTPAYMENT._serialized_end=2645
- _BTCSCRIPTCONFIGREGISTRATION._serialized_start=2705
- _BTCSCRIPTCONFIGREGISTRATION._serialized_end=2858
- _BTCSUCCESS._serialized_start=2860
- _BTCSUCCESS._serialized_end=2872
- _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_start=2874
- _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_end=2983
- _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_start=2985
- _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_end=3045
- _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_start=3048
- _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_end=3300
- _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_start=3251
- _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_end=3300
- _BTCPREVTXINITREQUEST._serialized_start=3302
- _BTCPREVTXINITREQUEST._serialized_end=3400
- _BTCPREVTXINPUTREQUEST._serialized_start=3402
- _BTCPREVTXINPUTREQUEST._serialized_end=3516
- _BTCPREVTXOUTPUTREQUEST._serialized_start=3518
- _BTCPREVTXOUTPUTREQUEST._serialized_end=3580
- _BTCPAYMENTREQUESTREQUEST._serialized_start=3583
- _BTCPAYMENTREQUESTREQUEST._serialized_end=3882
- _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_start=3759
- _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_end=3882
- _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_start=3850
- _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_end=3874
- _BTCSIGNMESSAGEREQUEST._serialized_start=3885
- _BTCSIGNMESSAGEREQUEST._serialized_end=4123
- _BTCSIGNMESSAGERESPONSE._serialized_start=4125
- _BTCSIGNMESSAGERESPONSE._serialized_end=4168
- _BTCREQUEST._serialized_start=4171
- _BTCREQUEST._serialized_end=4812
- _BTCRESPONSE._serialized_start=4815
- _BTCRESPONSE._serialized_end=5215
+ _BTCXPUBSREQUEST._serialized_start=1036
+ _BTCXPUBSREQUEST._serialized_end=1259
+ _BTCXPUBSREQUEST_XPUBTYPE._serialized_start=1216
+ _BTCXPUBSREQUEST_XPUBTYPE._serialized_end=1259
+ _BTCSCRIPTCONFIGWITHKEYPATH._serialized_start=1261
+ _BTCSCRIPTCONFIGWITHKEYPATH._serialized_end=1368
+ _BTCSIGNINITREQUEST._serialized_start=1371
+ _BTCSIGNINITREQUEST._serialized_end=1818
+ _BTCSIGNINITREQUEST_FORMATUNIT._serialized_start=1784
+ _BTCSIGNINITREQUEST_FORMATUNIT._serialized_end=1818
+ _BTCSIGNNEXTRESPONSE._serialized_start=1821
+ _BTCSIGNNEXTRESPONSE._serialized_end=2273
+ _BTCSIGNNEXTRESPONSE_TYPE._serialized_start=2143
+ _BTCSIGNNEXTRESPONSE_TYPE._serialized_end=2273
+ _BTCSIGNINPUTREQUEST._serialized_start=2276
+ _BTCSIGNINPUTREQUEST._serialized_end=2510
+ _BTCSIGNOUTPUTREQUEST._serialized_start=2513
+ _BTCSIGNOUTPUTREQUEST._serialized_end=2928
+ _BTCSIGNOUTPUTREQUEST_SILENTPAYMENT._serialized_start=2839
+ _BTCSIGNOUTPUTREQUEST_SILENTPAYMENT._serialized_end=2871
+ _BTCSCRIPTCONFIGREGISTRATION._serialized_start=2931
+ _BTCSCRIPTCONFIGREGISTRATION._serialized_end=3084
+ _BTCSUCCESS._serialized_start=3086
+ _BTCSUCCESS._serialized_end=3098
+ _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_start=3100
+ _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_end=3209
+ _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_start=3211
+ _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_end=3271
+ _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_start=3274
+ _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_end=3526
+ _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_start=3477
+ _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_end=3526
+ _BTCPREVTXINITREQUEST._serialized_start=3528
+ _BTCPREVTXINITREQUEST._serialized_end=3626
+ _BTCPREVTXINPUTREQUEST._serialized_start=3628
+ _BTCPREVTXINPUTREQUEST._serialized_end=3742
+ _BTCPREVTXOUTPUTREQUEST._serialized_start=3744
+ _BTCPREVTXOUTPUTREQUEST._serialized_end=3806
+ _BTCPAYMENTREQUESTREQUEST._serialized_start=3809
+ _BTCPAYMENTREQUESTREQUEST._serialized_end=4108
+ _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_start=3985
+ _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_end=4108
+ _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_start=4076
+ _BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_end=4100
+ _BTCSIGNMESSAGEREQUEST._serialized_start=4111
+ _BTCSIGNMESSAGEREQUEST._serialized_end=4349
+ _BTCSIGNMESSAGERESPONSE._serialized_start=4351
+ _BTCSIGNMESSAGERESPONSE._serialized_end=4394
+ _BTCREQUEST._serialized_start=4397
+ _BTCREQUEST._serialized_end=5094
+ _BTCRESPONSE._serialized_start=5097
+ _BTCRESPONSE._serialized_end=5549
# @@protoc_insertion_point(module_scope)
diff --git a/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
index acf1f54..05022bd 100644
--- a/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
+++ b/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
@@ -263,6 +263,43 @@ class BTCPubRequest(google.protobuf.message.Message):
global___BTCPubRequest = BTCPubRequest
+@typing.final
+class BTCXpubsRequest(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ class _XPubType:
+ ValueType = typing.NewType("ValueType", builtins.int)
+ V: typing_extensions.TypeAlias = ValueType
+
+ class _XPubTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCXpubsRequest._XPubType.ValueType], builtins.type):
+ DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+ UNKNOWN: BTCXpubsRequest._XPubType.ValueType # 0
+ XPUB: BTCXpubsRequest._XPubType.ValueType # 1
+ TPUB: BTCXpubsRequest._XPubType.ValueType # 2
+
+ class XPubType(_XPubType, metaclass=_XPubTypeEnumTypeWrapper): ...
+ UNKNOWN: BTCXpubsRequest.XPubType.ValueType # 0
+ XPUB: BTCXpubsRequest.XPubType.ValueType # 1
+ TPUB: BTCXpubsRequest.XPubType.ValueType # 2
+
+ COIN_FIELD_NUMBER: builtins.int
+ XPUB_TYPE_FIELD_NUMBER: builtins.int
+ KEYPATHS_FIELD_NUMBER: builtins.int
+ coin: global___BTCCoin.ValueType
+ xpub_type: global___BTCXpubsRequest.XPubType.ValueType
+ @property
+ def keypaths(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[common_pb2.Keypath]: ...
+ def __init__(
+ self,
+ *,
+ coin: global___BTCCoin.ValueType = ...,
+ xpub_type: global___BTCXpubsRequest.XPubType.ValueType = ...,
+ keypaths: collections.abc.Iterable[common_pb2.Keypath] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["coin", b"coin", "keypaths", b"keypaths", "xpub_type", b"xpub_type"]) -> None: ...
+
+global___BTCXpubsRequest = BTCXpubsRequest
+
@typing.final
class BTCScriptConfigWithKeypath(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
@@ -826,6 +863,7 @@ class BTCRequest(google.protobuf.message.Message):
SIGN_MESSAGE_FIELD_NUMBER: builtins.int
ANTIKLEPTO_SIGNATURE_FIELD_NUMBER: builtins.int
PAYMENT_REQUEST_FIELD_NUMBER: builtins.int
+ XPUBS_FIELD_NUMBER: builtins.int
@property
def is_script_config_registered(self) -> global___BTCIsScriptConfigRegisteredRequest: ...
@property
@@ -842,6 +880,8 @@ class BTCRequest(google.protobuf.message.Message):
def antiklepto_signature(self) -> antiklepto_pb2.AntiKleptoSignatureRequest: ...
@property
def payment_request(self) -> global___BTCPaymentRequestRequest: ...
+ @property
+ def xpubs(self) -> global___BTCXpubsRequest: ...
def __init__(
self,
*,
@@ -853,10 +893,11 @@ class BTCRequest(google.protobuf.message.Message):
sign_message: global___BTCSignMessageRequest | None = ...,
antiklepto_signature: antiklepto_pb2.AntiKleptoSignatureRequest | None = ...,
payment_request: global___BTCPaymentRequestRequest | None = ...,
+ xpubs: global___BTCXpubsRequest | None = ...,
) -> None: ...
- 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: ...
- 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: ...
- 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: ...
+ 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: ...
+ 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: ...
+ 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: ...
global___BTCRequest = BTCRequest
@@ -869,6 +910,7 @@ class BTCResponse(google.protobuf.message.Message):
SIGN_NEXT_FIELD_NUMBER: builtins.int
SIGN_MESSAGE_FIELD_NUMBER: builtins.int
ANTIKLEPTO_SIGNER_COMMITMENT_FIELD_NUMBER: builtins.int
+ PUBS_FIELD_NUMBER: builtins.int
@property
def success(self) -> global___BTCSuccess: ...
@property
@@ -879,6 +921,8 @@ class BTCResponse(google.protobuf.message.Message):
def sign_message(self) -> global___BTCSignMessageResponse: ...
@property
def antiklepto_signer_commitment(self) -> antiklepto_pb2.AntiKleptoSignerCommitment: ...
+ @property
+ def pubs(self) -> common_pb2.PubsResponse: ...
def __init__(
self,
*,
@@ -887,9 +931,10 @@ class BTCResponse(google.protobuf.message.Message):
sign_next: global___BTCSignNextResponse | None = ...,
sign_message: global___BTCSignMessageResponse | None = ...,
antiklepto_signer_commitment: antiklepto_pb2.AntiKleptoSignerCommitment | None = ...,
+ pubs: common_pb2.PubsResponse | None = ...,
) -> None: ...
- 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: ...
- 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: ...
- 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: ...
+ 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: ...
+ 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: ...
+ 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: ...
global___BTCResponse = BTCResponse
diff --git a/py/bitbox02/bitbox02/communication/generated/common_pb2.py b/py/bitbox02/bitbox02/communication/generated/common_pb2.py
index 3109f6e..d0e55cf 100644
--- a/py/bitbox02/bitbox02/communication/generated/common_pb2.py
+++ b/py/bitbox02/bitbox02/communication/generated/common_pb2.py
@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63ommon.proto\x12\x14shiftcrypto.bitbox02\"\x1a\n\x0bPubResponse\x12\x0b\n\x03pub\x18\x01 \x01(\t\"\x18\n\x16RootFingerprintRequest\".\n\x17RootFingerprintResponse\x12\x13\n\x0b\x66ingerprint\x18\x01 \x01(\x0c\"l\n\x04XPub\x12\r\n\x05\x64\x65pth\x18\x01 \x01(\x0c\x12\x1a\n\x12parent_fingerprint\x18\x02 \x01(\x0c\x12\x11\n\tchild_num\x18\x03 \x01(\r\x12\x12\n\nchain_code\x18\x04 \x01(\x0c\x12\x12\n\npublic_key\x18\x05 \x01(\x0c\"\x1a\n\x07Keypath\x12\x0f\n\x07keypath\x18\x01 \x03(\r\"d\n\rKeyOriginInfo\x12\x18\n\x10root_fingerprint\x18\x01 \x01(\x0c\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12(\n\x04xpub\x18\x03 \x01(\x0b\x32\x1a.shiftcrypto.bitbox02.XPubb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63ommon.proto\x12\x14shiftcrypto.bitbox02\"\x1a\n\x0bPubResponse\x12\x0b\n\x03pub\x18\x01 \x01(\t\"\x1c\n\x0cPubsResponse\x12\x0c\n\x04pubs\x18\x01 \x03(\t\"\x18\n\x16RootFingerprintRequest\".\n\x17RootFingerprintResponse\x12\x13\n\x0b\x66ingerprint\x18\x01 \x01(\x0c\"l\n\x04XPub\x12\r\n\x05\x64\x65pth\x18\x01 \x01(\x0c\x12\x1a\n\x12parent_fingerprint\x18\x02 \x01(\x0c\x12\x11\n\tchild_num\x18\x03 \x01(\r\x12\x12\n\nchain_code\x18\x04 \x01(\x0c\x12\x12\n\npublic_key\x18\x05 \x01(\x0c\"\x1a\n\x07Keypath\x12\x0f\n\x07keypath\x18\x01 \x03(\r\"d\n\rKeyOriginInfo\x12\x18\n\x10root_fingerprint\x18\x01 \x01(\x0c\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12(\n\x04xpub\x18\x03 \x01(\x0b\x32\x1a.shiftcrypto.bitbox02.XPubb\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'common_pb2', globals())
@@ -22,14 +22,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_PUBRESPONSE._serialized_start=38
_PUBRESPONSE._serialized_end=64
- _ROOTFINGERPRINTREQUEST._serialized_start=66
- _ROOTFINGERPRINTREQUEST._serialized_end=90
- _ROOTFINGERPRINTRESPONSE._serialized_start=92
- _ROOTFINGERPRINTRESPONSE._serialized_end=138
- _XPUB._serialized_start=140
- _XPUB._serialized_end=248
- _KEYPATH._serialized_start=250
- _KEYPATH._serialized_end=276
- _KEYORIGININFO._serialized_start=278
- _KEYORIGININFO._serialized_end=378
+ _PUBSRESPONSE._serialized_start=66
+ _PUBSRESPONSE._serialized_end=94
+ _ROOTFINGERPRINTREQUEST._serialized_start=96
+ _ROOTFINGERPRINTREQUEST._serialized_end=120
+ _ROOTFINGERPRINTRESPONSE._serialized_start=122
+ _ROOTFINGERPRINTRESPONSE._serialized_end=168
+ _XPUB._serialized_start=170
+ _XPUB._serialized_end=278
+ _KEYPATH._serialized_start=280
+ _KEYPATH._serialized_end=306
+ _KEYORIGININFO._serialized_start=308
+ _KEYORIGININFO._serialized_end=408
# @@protoc_insertion_point(module_scope)
diff --git a/py/bitbox02/bitbox02/communication/generated/common_pb2.pyi b/py/bitbox02/bitbox02/communication/generated/common_pb2.pyi
index 4e73c7e..4e65dfe 100644
--- a/py/bitbox02/bitbox02/communication/generated/common_pb2.pyi
+++ b/py/bitbox02/bitbox02/communication/generated/common_pb2.pyi
@@ -40,6 +40,22 @@ class PubResponse(google.protobuf.message.Message):
global___PubResponse = PubResponse
+@typing.final
+class PubsResponse(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ PUBS_FIELD_NUMBER: builtins.int
+ @property
+ def pubs(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: ...
+ def __init__(
+ self,
+ *,
+ pubs: collections.abc.Iterable[builtins.str] | None = ...,
+ ) -> None: ...
+ def ClearField(self, field_name: typing.Literal["pubs", b"pubs"]) -> None: ...
+
+global___PubsResponse = PubsResponse
+
@typing.final
class RootFingerprintRequest(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
diff --git a/py/send_message.py b/py/send_message.py
index 3648141..a611c27 100755
--- a/py/send_message.py
+++ b/py/send_message.py
@@ -331,6 +331,14 @@ class SendMessage:
except UserAbortException:
eprint("Aborted by user")
+ def _btc_xpubs(self) -> None:
+ xpubs = self._device.btc_xpubs(
+ keypaths=[[84 + HARDENED, 0 + HARDENED, i + HARDENED] for i in range(20)],
+ )
+ print("xpubs for m/84'/0'/{0'..19'}:")
+ for xpub in xpubs:
+ print(xpub)
+
def _get_electrum_encryption_key(self) -> None:
print(
"Electrum wallet encryption xpub at keypath m/4541509'/1112098098':",
@@ -376,7 +384,7 @@ class SendMessage:
my_xpub = self._device.btc_xpub(
keypath=account_keypath,
coin=coin,
- xpub_type=bitbox02.btc.BTCPubRequest.XPUB, # pylint: disable=no-member,
+ xpub_type=bitbox02.btc.BTCPubRequest.XPUB,
display=False,
)
multisig_config = bitbox02.btc.BTCScriptConfig(
@@ -1503,6 +1511,7 @@ class SendMessage:
("Change device name", self._change_name_workflow),
("Get root fingerprint", self._get_root_fingerprint),
("Retrieve zpub of first account", self._display_zpub),
+ ("Retrieve multiple xpubs", self._btc_xpubs),
("Retrieve a BTC address", self._btc_address),
("Retrieve a BTC Multisig address", self._btc_multisig_address),
("Retrieve a BTC policy address", self._btc_policy_address),
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
index 29c16c8..d1035d3 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin.rs
@@ -28,6 +28,7 @@ mod script;
mod script_configs;
pub mod signmsg;
pub mod signtx;
+mod xpubs;
use super::Error;
use super::pb;
@@ -315,6 +316,7 @@ pub async fn process_api(
registration::process_register_script_config(hal, request).await
}
Request::SignMessage(request) => signmsg::process(hal, request).await,
+ Request::Xpubs(request) => xpubs::process_xpubs(request).await,
// These are streamed asynchronously using the `next_request()` primitive in
// bitcoin/signtx.rs and are not handled directly.
Request::PrevtxInit(_)
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/xpubs.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/xpubs.rs
new file mode 100644
index 0000000..4b1e85e
--- /dev/null
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/xpubs.rs
@@ -0,0 +1,196 @@
+// 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.
+
+use super::Error;
+use super::params;
+use super::pb;
+
+use pb::BtcCoin;
+use pb::btc_response::Response;
+use pb::btc_xpubs_request::XPubType;
+
+use alloc::string::String;
+use alloc::vec::Vec;
+
+/// Max number of xpubs that can be requested at once.
+const MAX_XPUBS: usize = 20;
+
+/// Retrieves up to 20 xpubs at once.
+///
+/// Only standard keypaths are allowed for now.
+pub async fn process_xpubs(request: &pb::BtcXpubsRequest) -> Result<Response, Error> {
+ let coin = BtcCoin::try_from(request.coin)?;
+ super::coin_enabled(coin)?;
+
+ let params = params::get(coin);
+ if request.keypaths.len() > MAX_XPUBS {
+ return Err(Error::InvalidInput);
+ }
+ let xpub_type: pb::btc_pub_request::XPubType = {
+ let xpub_type = XPubType::try_from(request.xpub_type).map_err(|_| Error::InvalidInput)?;
+ match xpub_type {
+ XPubType::Unknown => return Err(Error::InvalidInput),
+ XPubType::Xpub => pb::btc_pub_request::XPubType::Xpub,
+ XPubType::Tpub => pb::btc_pub_request::XPubType::Tpub,
+ }
+ };
+ let keypaths: Vec<&[u32]> = request
+ .keypaths
+ .iter()
+ .map(|k| k.keypath.as_slice())
+ .collect();
+
+ for keypath in keypaths.iter() {
+ super::keypath::validate_xpub(keypath, params.bip44_coin, params.taproot_support)
+ .map_err(|_| Error::InvalidInput)?;
+ }
+
+ let xpubs = crate::keystore::get_xpubs_twice(&keypaths)?;
+ let xpub_strings: Vec<String> = xpubs
+ .iter()
+ .map(|xpub| xpub.serialize_str(xpub_type))
+ .collect::<Result<_, _>>()?;
+ Ok(Response::Pubs(pb::PubsResponse { pubs: xpub_strings }))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use crate::bb02_async::block_on;
+ use bitbox02::testing::{mock_memory, mock_unlocked, mock_unlocked_using_mnemonic};
+ use util::bip32::HARDENED;
+
+ #[test]
+ pub fn test_process_xpubs() {
+ mock_unlocked_using_mnemonic(
+ "sleep own lobster state clean thrive tail exist cactus bitter pass soccer clinic riot dream turkey before sport action praise tunnel hood donate man",
+ "",
+ );
+
+ bitbox02::securechip::fake_event_counter_reset();
+ assert_eq!(
+ block_on(process_xpubs(&pb::BtcXpubsRequest {
+ coin: BtcCoin::Btc as _,
+ xpub_type: XPubType::Xpub as _,
+ keypaths: vec![
+ pb::Keypath {
+ keypath: vec![84 + HARDENED, HARDENED, HARDENED],
+ },
+ pb::Keypath {
+ keypath: vec![86 + HARDENED, HARDENED, HARDENED],
+ },
+ pb::Keypath {
+ keypath: vec![49 + HARDENED, HARDENED, HARDENED],
+ },
+ ],
+ })),
+ Ok(Response::Pubs(pb::PubsResponse {
+ pubs: vec![
+ "xpub6CNbmcHwZDudAvCAZVE5kejUoFD63mbkRbRMA2HoF9oNWsCofni87gJKp31qZJ9FsCMQR2vK9AS51mT8dgUMGsHW6SfaAKb4eSzpqJn7zwK".into(),
+ "xpub6CGwpj8iQNuzSeeEKF4yuQt32fpLqfHj7sUfFH4uW34DoctWPksxAdjNYC9KwYgwA149B7SDdcLH1aFmucRcjBL4U6piN7HgaiFCBsToamH".into(),
+ "xpub6CKWKetFeZaZm76Tmzymyadg2Lc9njNZvCV7XaeePLbwPatyVTqS3k8iWeJNziZR6n1kUqkChCmaP7MxyED3KDsSUH7F5Lc9RFe9P4B78Uc".into(),
+ ]
+ })),
+ );
+ assert_eq!(bitbox02::securechip::fake_event_counter(), 2);
+
+ // Different output type
+ assert_eq!(
+ block_on(process_xpubs(&pb::BtcXpubsRequest {
+ coin: BtcCoin::Btc as _,
+ xpub_type: XPubType::Tpub as _,
+ keypaths: vec![
+ pb::Keypath {
+ keypath: vec![84 + HARDENED, HARDENED, HARDENED],
+ },
+ ],
+ })),
+ Ok(Response::Pubs(pb::PubsResponse {
+ pubs: vec![
+ "tpubDCkEHr7dGVs5SiP21gDAxa4r8NJk3A6oyE1eWaLwb4ZGG9sWk1ZDG7yA456d5o6Vf6tK2cSBgGG7hwwk2YKbAJjoA3QsqrFJEQbEbLKkt5w".into(),
+ ]
+ })),
+ );
+
+ // Different coin
+ assert_eq!(
+ block_on(process_xpubs(&pb::BtcXpubsRequest {
+ coin: BtcCoin::Ltc as _,
+ xpub_type: XPubType::Xpub as _,
+ keypaths: vec![
+ pb::Keypath {
+ keypath: vec![84 + HARDENED, 2+HARDENED, HARDENED],
+ },
+ ],
+ })),
+ Ok(Response::Pubs(pb::PubsResponse {
+ pubs: vec![
+ "xpub6DEKPXTV5HQNcJNGWcSCsdEc2zzoXUHy1L678r3ux3CN2iHqxwKgFaxnzs73nr33VR7SNTDaqFzeyMwHocBEa4j96LEoKacL38N6RAXS3hP".into(),
+ ]
+ })),
+ );
+ }
+
+ // Can get up to 20 xpubs and not more..
+ #[test]
+ pub fn test_process_limit() {
+ mock_unlocked();
+
+ // At limit
+ let result = block_on(process_xpubs(&pb::BtcXpubsRequest {
+ coin: BtcCoin::Btc as _,
+ xpub_type: XPubType::Xpub as _,
+ keypaths: (0..20)
+ .map(|i| pb::Keypath {
+ keypath: vec![86 + HARDENED, HARDENED, HARDENED + i],
+ })
+ .collect(),
+ }))
+ .unwrap();
+ match result {
+ Response::Pubs(pubs) => assert_eq!(pubs.pubs.len(), 20),
+ _ => panic!("unexpeced response"),
+ };
+
+ // Over limit
+ assert_eq!(
+ block_on(process_xpubs(&pb::BtcXpubsRequest {
+ coin: BtcCoin::Btc as _,
+ xpub_type: XPubType::Xpub as _,
+ keypaths: (0..21)
+ .map(|i| pb::Keypath {
+ keypath: vec![86 + HARDENED, HARDENED, HARDENED + i],
+ })
+ .collect(),
+ })),
+ Err(Error::InvalidInput)
+ );
+ }
+
+ #[test]
+ pub fn test_process_invalid_keypath() {
+ mock_unlocked();
+ assert_eq!(
+ block_on(process_xpubs(&pb::BtcXpubsRequest {
+ coin: BtcCoin::Ltc as _,
+ xpub_type: XPubType::Xpub as _,
+ keypaths: vec![pb::Keypath {
+ keypath: vec![84 + HARDENED, 0 + HARDENED, HARDENED],
+ },],
+ })),
+ Err(Error::InvalidInput),
+ );
+ }
+}
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index e6be355..00ca0fa 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -85,6 +85,46 @@ pub fn get_xpub_twice(keypath: &[u32]) -> Result<bip32::Xpub, ()> {
Ok(res1)
}
+/// Gets multiple xpubs at once. This is better than multiple calls to `get_xpub_twice()` as it only
+/// uses two secure chip operations in total, instead of two per xpub.
+pub fn get_xpubs_twice(keypaths: &[&[u32]]) -> Result<Vec<bip32::Xpub>, ()> {
+ if keystore::is_locked() {
+ return Err(());
+ }
+ if keypaths.is_empty() {
+ return Ok(vec![]);
+ }
+ // We get the root xprv as a starting point (twice to mitigate bitflips), afterwards we don't
+ // need the securechip anymore.
+ let xprv = get_xprv(&[])?;
+ let xprv2 = get_xprv(&[])?;
+
+ let mut out = Vec::with_capacity(keypaths.len());
+ for keypath in keypaths {
+ if xprv != xprv2 {
+ return Err(());
+ }
+
+ let derive_xpub = || -> Result<bip32::Xpub, ()> {
+ let derived_xprv = xprv
+ .xprv
+ .derive_priv(SECP256K1, &bip32::keypath_from_slice(keypath))
+ .map_err(|_| ())?;
+ Ok(bip32::Xpub::from(bitcoin::bip32::Xpub::from_priv(
+ SECP256K1,
+ &derived_xprv,
+ )))
+ };
+ let derived_xpub = derive_xpub()?;
+ if derived_xpub != derive_xpub()? {
+ return Err(());
+ }
+ out.push(derived_xpub);
+ }
+
+ Ok(out)
+}
+
/// Returns fingerprint of the root public key at m/, which are the first four bytes of its hash160
/// according to:
/// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#serialization-format
@@ -330,6 +370,43 @@ mod tests {
)
}
+ #[test]
+ fn test_get_xpubs_twice() {
+ keystore::lock();
+ assert!(get_xpubs_twice(&[]).is_err());
+
+ mock_unlocked_using_mnemonic(
+ "sleep own lobster state clean thrive tail exist cactus bitter pass soccer clinic riot dream turkey before sport action praise tunnel hood donate man",
+ "",
+ );
+
+ // Helper to convert to strings.
+ let get = |keypaths| -> Vec<String> {
+ get_xpubs_twice(keypaths)
+ .unwrap()
+ .iter()
+ .map(|xpub| xpub.serialize_str(bip32::XPubType::Xpub).unwrap())
+ .collect()
+ };
+
+ bitbox02::securechip::fake_event_counter_reset();
+ assert!(get_xpubs_twice(&[]).unwrap().is_empty());
+ assert_eq!(bitbox02::securechip::fake_event_counter(), 0);
+
+ bitbox02::securechip::fake_event_counter_reset();
+ assert_eq!(
+ get(&[
+ &[84 + HARDENED, HARDENED, HARDENED],
+ &[86 + HARDENED, HARDENED, HARDENED],
+ ]),
+ vec![
+ "xpub6CNbmcHwZDudAvCAZVE5kejUoFD63mbkRbRMA2HoF9oNWsCofni87gJKp31qZJ9FsCMQR2vK9AS51mT8dgUMGsHW6SfaAKb4eSzpqJn7zwK",
+ "xpub6CGwpj8iQNuzSeeEKF4yuQt32fpLqfHj7sUfFH4uW34DoctWPksxAdjNYC9KwYgwA149B7SDdcLH1aFmucRcjBL4U6piN7HgaiFCBsToamH",
+ ],
+ );
+ assert_eq!(bitbox02::securechip::fake_event_counter(), 2);
+ }
+
#[test]
fn test_root_fingerprint() {
keystore::lock();
diff --git a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
index 888f01a..018adf9 100644
--- a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
+++ b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
@@ -6,6 +6,12 @@ pub struct PubResponse {
pub r#pub: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct PubsResponse {
+ #[prost(string, repeated, tag = "1")]
+ pub pubs: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct RootFingerprintRequest {}
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -509,6 +515,58 @@ pub mod btc_pub_request {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct BtcXpubsRequest {
+ #[prost(enumeration = "BtcCoin", tag = "1")]
+ pub coin: i32,
+ #[prost(enumeration = "btc_xpubs_request::XPubType", tag = "2")]
+ pub xpub_type: i32,
+ #[prost(message, repeated, tag = "3")]
+ pub keypaths: ::prost::alloc::vec::Vec<Keypath>,
+}
+/// Nested message and enum types in `BTCXpubsRequest`.
+pub mod btc_xpubs_request {
+ #[derive(
+ Clone,
+ Copy,
+ Debug,
+ PartialEq,
+ Eq,
+ Hash,
+ PartialOrd,
+ Ord,
+ ::prost::Enumeration
+ )]
+ #[repr(i32)]
+ pub enum XPubType {
+ Unknown = 0,
+ Xpub = 1,
+ Tpub = 2,
+ }
+ impl XPubType {
+ /// String value of the enum field names used in the ProtoBuf definition.
+ ///
+ /// The values are not transformed in any way and thus are considered stable
+ /// (if the ProtoBuf definition does not change) and safe for programmatic use.
+ pub fn as_str_name(&self) -> &'static str {
+ match self {
+ XPubType::Unknown => "UNKNOWN",
+ XPubType::Xpub => "XPUB",
+ XPubType::Tpub => "TPUB",
+ }
+ }
+ /// Creates an enum from field names used in the ProtoBuf definition.
+ pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
+ match value {
+ "UNKNOWN" => Some(Self::Unknown),
+ "XPUB" => Some(Self::Xpub),
+ "TPUB" => Some(Self::Tpub),
+ _ => None,
+ }
+ }
+ }
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct BtcScriptConfigWithKeypath {
#[prost(message, optional, tag = "2")]
pub script_config: ::core::option::Option<BtcScriptConfig>,
@@ -904,7 +962,7 @@ pub struct BtcSignMessageResponse {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct BtcRequest {
- #[prost(oneof = "btc_request::Request", tags = "1, 2, 3, 4, 5, 6, 7, 8")]
+ #[prost(oneof = "btc_request::Request", tags = "1, 2, 3, 4, 5, 6, 7, 8, 9")]
pub request: ::core::option::Option<btc_request::Request>,
}
/// Nested message and enum types in `BTCRequest`.
@@ -928,12 +986,14 @@ pub mod btc_request {
AntikleptoSignature(super::AntiKleptoSignatureRequest),
#[prost(message, tag = "8")]
PaymentRequest(super::BtcPaymentRequestRequest),
+ #[prost(message, tag = "9")]
+ Xpubs(super::BtcXpubsRequest),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct BtcResponse {
- #[prost(oneof = "btc_response::Response", tags = "1, 2, 3, 4, 5")]
+ #[prost(oneof = "btc_response::Response", tags = "1, 2, 3, 4, 5, 6")]
pub response: ::core::option::Option<btc_response::Response>,
}
/// Nested message and enum types in `BTCResponse`.
@@ -951,6 +1011,8 @@ pub mod btc_response {
SignMessage(super::BtcSignMessageResponse),
#[prost(message, tag = "5")]
AntikleptoSignerCommitment(super::AntiKleptoSignerCommitment),
+ #[prost(message, tag = "6")]
+ Pubs(super::PubsResponse),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
Why this scored 30/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.