Btc: Add btc address derivation to coin purchase memo in payment request
What changed, and why it matters
This commit adds support for deriving and verifying Bitcoin-family destination addresses inside payment-request memos used for coin swaps. It extends the existing Ethereum-only address-derivation feature to also cover BTC/LTC, and validates that the claimed swap destination address actually matches the derived one. There is no direct evidence this fixes an active security bug; it appears to be a feature addition that also hardens validation.
Review as a hardening/feature commit. Ensure the new derivation path is covered by the same keypath-whitelisting and script-config registration checks applied elsewhere in the Bitcoin API, and confirm that downstream host libraries reject unknown memo variants gracefully for backward compatibility.
Security signals we found
New address-derivation validation path for BTC/LTC payment-request memos
Address mismatch check between derived address and memo-supplied address
Input validation: coin_type/unit consistency, simple-single-sig only, hardened account keypath element
Feature-gated under app-litecoin / app-ethereum compile flags
Extends protobuf oneof and generated bindings
Evidence from the diff
The change extends BTCPaymentRequestRequest.Memo.CoinPurchaseMemo with a new BtcAddressDerivation oneof variant carrying a BTCScriptConfigWithKeypath. The Rust firmware now derives the destination address for BTC/LTC simple-single-sig script configs and compares it against the address in the memo, returning ValidationError::AddressMismatch if they differ. It also enforces coin_type/unit consistency (BTC vs LTC) and rejects multisig or non-simple script configs. UI confirmation code uses the derived account index for the ‘Receive to’ screen.
Changed components
messages/btc.protopy/bitbox02/bitbox02/communication/generated/btc_pb2.pypy/bitbox02/bitbox02/communication/generated/btc_pb2.pyisrc/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rssrc/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rsInspect captured patch +520 / −41
diff --git a/messages/btc.proto b/messages/btc.proto
index 97ed257..6aa0727 100644
--- a/messages/btc.proto
+++ b/messages/btc.proto
@@ -255,8 +255,13 @@ message BTCPaymentRequestRequest {
message EthAddressDerivation {
repeated uint32 keypath = 1; // Keypath to the address
}
+ message BtcAddressDerivation {
+ // Script config + keypath are needed to derive BTC/LTC-family addresses.
+ BTCScriptConfigWithKeypath script_config = 1;
+ }
oneof address_derivation {
EthAddressDerivation eth = 4;
+ BtcAddressDerivation btc = 5;
}
}
oneof memo {
diff --git a/py/bitbox02/bitbox02/communication/generated/btc_pb2.py b/py/bitbox02/bitbox02/communication/generated/btc_pb2.py
index 25d1e6d..bbaebcb 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\"\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\"\x82\x05\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\xd1\x03\n\x04Memo\x12Q\n\ttext_memo\x18\x01 \x01(\x0b\x32<.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.TextMemoH\x00\x12\x62\n\x12\x63oin_purchase_memo\x18\x02 \x01(\x0b\x32\x44.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemoH\x00\x1a\x18\n\x08TextMemo\x12\x0c\n\x04note\x18\x01 \x01(\t\x1a\xef\x01\n\x10\x43oinPurchaseMemo\x12\x11\n\tcoin_type\x18\x01 \x01(\r\x12\x0e\n\x06\x61mount\x18\x02 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\t\x12h\n\x03\x65th\x18\x04 \x01(\x0b\x32Y.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.EthAddressDerivationH\x00\x1a\'\n\x14\x45thAddressDerivation\x12\x0f\n\x07keypath\x18\x01 \x03(\rB\x14\n\x12\x61\x64\x64ress_derivationB\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*a\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\x12\r\n\tOP_RETURN\x10\x06\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\"\xcd\x06\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\x9c\x05\n\x04Memo\x12Q\n\ttext_memo\x18\x01 \x01(\x0b\x32<.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.TextMemoH\x00\x12\x62\n\x12\x63oin_purchase_memo\x18\x02 \x01(\x0b\x32\x44.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemoH\x00\x1a\x18\n\x08TextMemo\x12\x0c\n\x04note\x18\x01 \x01(\t\x1a\xba\x03\n\x10\x43oinPurchaseMemo\x12\x11\n\tcoin_type\x18\x01 \x01(\r\x12\x0e\n\x06\x61mount\x18\x02 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\t\x12h\n\x03\x65th\x18\x04 \x01(\x0b\x32Y.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.EthAddressDerivationH\x00\x12h\n\x03\x62tc\x18\x05 \x01(\x0b\x32Y.shiftcrypto.bitbox02.BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.BtcAddressDerivationH\x00\x1a\'\n\x14\x45thAddressDerivation\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x1a_\n\x14\x42tcAddressDerivation\x12G\n\rscript_config\x18\x01 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypathB\x14\n\x12\x61\x64\x64ress_derivationB\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*a\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\x12\r\n\tOP_RETURN\x10\x06\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc_pb2', globals())
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
- _BTCCOIN._serialized_start=5894
- _BTCCOIN._serialized_end=5951
- _BTCOUTPUTTYPE._serialized_start=5953
- _BTCOUTPUTTYPE._serialized_end=6050
+ _BTCCOIN._serialized_start=6097
+ _BTCCOIN._serialized_end=6154
+ _BTCOUTPUTTYPE._serialized_start=6156
+ _BTCOUTPUTTYPE._serialized_end=6253
_BTCSCRIPTCONFIG._serialized_start=68
_BTCSCRIPTCONFIG._serialized_end=650
_BTCSCRIPTCONFIG_MULTISIG._serialized_start=293
@@ -79,21 +79,23 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_BTCPREVTXOUTPUTREQUEST._serialized_start=3744
_BTCPREVTXOUTPUTREQUEST._serialized_end=3806
_BTCPAYMENTREQUESTREQUEST._serialized_start=3809
- _BTCPAYMENTREQUESTREQUEST._serialized_end=4451
+ _BTCPAYMENTREQUESTREQUEST._serialized_end=4654
_BTCPAYMENTREQUESTREQUEST_MEMO._serialized_start=3986
- _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_end=4451
+ _BTCPAYMENTREQUESTREQUEST_MEMO._serialized_end=4654
_BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_start=4177
_BTCPAYMENTREQUESTREQUEST_MEMO_TEXTMEMO._serialized_end=4201
_BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_start=4204
- _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_end=4443
- _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_start=4382
- _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_end=4421
- _BTCSIGNMESSAGEREQUEST._serialized_start=4454
- _BTCSIGNMESSAGEREQUEST._serialized_end=4692
- _BTCSIGNMESSAGERESPONSE._serialized_start=4694
- _BTCSIGNMESSAGERESPONSE._serialized_end=4737
- _BTCREQUEST._serialized_start=4740
- _BTCREQUEST._serialized_end=5437
- _BTCRESPONSE._serialized_start=5440
- _BTCRESPONSE._serialized_end=5892
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO._serialized_end=4646
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_start=4488
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_ETHADDRESSDERIVATION._serialized_end=4527
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_BTCADDRESSDERIVATION._serialized_start=4529
+ _BTCPAYMENTREQUESTREQUEST_MEMO_COINPURCHASEMEMO_BTCADDRESSDERIVATION._serialized_end=4624
+ _BTCSIGNMESSAGEREQUEST._serialized_start=4657
+ _BTCSIGNMESSAGEREQUEST._serialized_end=4895
+ _BTCSIGNMESSAGERESPONSE._serialized_start=4897
+ _BTCSIGNMESSAGERESPONSE._serialized_end=4940
+ _BTCREQUEST._serialized_start=4943
+ _BTCREQUEST._serialized_end=5640
+ _BTCRESPONSE._serialized_start=5643
+ _BTCRESPONSE._serialized_end=6095
# @@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 611723b..a207ee6 100644
--- a/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
+++ b/py/bitbox02/bitbox02/communication/generated/btc_pb2.pyi
@@ -784,10 +784,28 @@ class BTCPaymentRequestRequest(google.protobuf.message.Message):
) -> None: ...
def ClearField(self, field_name: typing.Literal["keypath", b"keypath"]) -> None: ...
+ @typing.final
+ class BtcAddressDerivation(google.protobuf.message.Message):
+ DESCRIPTOR: google.protobuf.descriptor.Descriptor
+
+ SCRIPT_CONFIG_FIELD_NUMBER: builtins.int
+ @property
+ def script_config(self) -> global___BTCScriptConfigWithKeypath:
+ """Script config + keypath are needed to derive BTC/LTC-family addresses."""
+
+ def __init__(
+ self,
+ *,
+ script_config: global___BTCScriptConfigWithKeypath | None = ...,
+ ) -> None: ...
+ def HasField(self, field_name: typing.Literal["script_config", b"script_config"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["script_config", b"script_config"]) -> None: ...
+
COIN_TYPE_FIELD_NUMBER: builtins.int
AMOUNT_FIELD_NUMBER: builtins.int
ADDRESS_FIELD_NUMBER: builtins.int
ETH_FIELD_NUMBER: builtins.int
+ BTC_FIELD_NUMBER: builtins.int
coin_type: builtins.int
"""SLIP-44 coin type"""
amount: builtins.str
@@ -796,6 +814,8 @@ class BTCPaymentRequestRequest(google.protobuf.message.Message):
"""Address to send the purchased coins to"""
@property
def eth(self) -> global___BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.EthAddressDerivation: ...
+ @property
+ def btc(self) -> global___BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.BtcAddressDerivation: ...
def __init__(
self,
*,
@@ -803,10 +823,11 @@ class BTCPaymentRequestRequest(google.protobuf.message.Message):
amount: builtins.str = ...,
address: builtins.str = ...,
eth: global___BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.EthAddressDerivation | None = ...,
+ btc: global___BTCPaymentRequestRequest.Memo.CoinPurchaseMemo.BtcAddressDerivation | None = ...,
) -> None: ...
- def HasField(self, field_name: typing.Literal["address_derivation", b"address_derivation", "eth", b"eth"]) -> builtins.bool: ...
- def ClearField(self, field_name: typing.Literal["address", b"address", "address_derivation", b"address_derivation", "amount", b"amount", "coin_type", b"coin_type", "eth", b"eth"]) -> None: ...
- def WhichOneof(self, oneof_group: typing.Literal["address_derivation", b"address_derivation"]) -> typing.Literal["eth"] | None: ...
+ def HasField(self, field_name: typing.Literal["address_derivation", b"address_derivation", "btc", b"btc", "eth", b"eth"]) -> builtins.bool: ...
+ def ClearField(self, field_name: typing.Literal["address", b"address", "address_derivation", b"address_derivation", "amount", b"amount", "btc", b"btc", "coin_type", b"coin_type", "eth", b"eth"]) -> None: ...
+ def WhichOneof(self, oneof_group: typing.Literal["address_derivation", b"address_derivation"]) -> typing.Literal["eth", "btc"] | None: ...
TEXT_MEMO_FIELD_NUMBER: builtins.int
COIN_PURCHASE_MEMO_FIELD_NUMBER: builtins.int
diff --git a/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs b/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
index 76a5443..51bacb1 100644
--- a/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/bitcoin/payment_request.rs
@@ -180,6 +180,25 @@ pub async fn user_verify(
.ok_or(Error::InvalidInput)?
+ 1
}
+ memo::coin_purchase_memo::AddressDerivation::Btc(btc) => {
+ if !matches!(
+ (coin_purchase_memo.coin_type, destination_unit),
+ (0, "BTC") | (2, "LTC")
+ ) {
+ return Err(Error::InvalidInput);
+ }
+
+ let script_config =
+ btc.script_config.as_ref().ok_or(Error::InvalidInput)?;
+
+ script_config
+ .keypath
+ .get(2)
+ .ok_or(Error::InvalidInput)?
+ .checked_sub(util::bip32::HARDENED)
+ .ok_or(Error::InvalidInput)?
+ + 1
+ }
};
hal.ui()
.confirm(&ConfirmParams {
@@ -335,6 +354,46 @@ pub fn validate(
#[cfg(not(feature = "app-ethereum"))]
return Err(ValidationError::Disabled);
}
+ Some(memo::coin_purchase_memo::AddressDerivation::Btc(_btc)) => {
+ #[cfg(feature = "app-litecoin")]
+ {
+ let (_, destination_unit) =
+ parse_coin_purchase_amount(&coin_purchase_memo.amount)
+ .map_err(|_| ValidationError::Other)?;
+
+ let destination_coin =
+ match (coin_purchase_memo.coin_type, destination_unit) {
+ (0, "BTC") => pb::BtcCoin::Btc,
+ (2, "LTC") => pb::BtcCoin::Ltc,
+ _ => return Err(ValidationError::Other),
+ };
+
+ let script_config =
+ _btc.script_config.as_ref().ok_or(ValidationError::Other)?;
+
+ let simple_type = match script_config.script_config.as_ref() {
+ Some(pb::BtcScriptConfig {
+ config: Some(pb::btc_script_config::Config::SimpleType(simple_type)),
+ }) => pb::btc_script_config::SimpleType::try_from(*simple_type)
+ .map_err(|_| ValidationError::Other)?,
+ _ => return Err(ValidationError::Other),
+ };
+
+ let derived_address = super::derive_address_simple(
+ hal,
+ destination_coin,
+ simple_type,
+ &script_config.keypath,
+ )
+ .map_err(|_| ValidationError::Other)?;
+
+ if derived_address != coin_purchase_memo.address {
+ return Err(ValidationError::AddressMismatch);
+ }
+ }
+ #[cfg(not(feature = "app-litecoin"))]
+ return Err(ValidationError::Disabled);
+ }
None => return Err(ValidationError::Other),
}
}
@@ -373,7 +432,7 @@ mod tests {
}
}
- #[cfg(feature = "app-ethereum")]
+ #[cfg(feature = "app-litecoin")]
fn dummy_eth_address_derivation(valid: bool) -> memo::coin_purchase_memo::AddressDerivation {
let coin_type = if valid { 60 } else { 0 };
memo::coin_purchase_memo::AddressDerivation::Eth(
@@ -389,6 +448,23 @@ mod tests {
)
}
+ #[cfg(feature = "app-ethereum")]
+ fn dummy_btc_address_derivation(
+ simple_type: pb::btc_script_config::SimpleType,
+ keypath: &[u32],
+ ) -> memo::coin_purchase_memo::AddressDerivation {
+ memo::coin_purchase_memo::AddressDerivation::Btc(
+ memo::coin_purchase_memo::BtcAddressDerivation {
+ script_config: Some(pb::BtcScriptConfigWithKeypath {
+ script_config: Some(pb::BtcScriptConfig {
+ config: Some(pb::btc_script_config::Config::SimpleType(simple_type as _)),
+ }),
+ keypath: keypath.to_vec(),
+ }),
+ },
+ )
+ }
+
#[test]
fn test_find_identity() {
assert_eq!(find_identity("POCKET").unwrap().name, "POCKET");
@@ -572,6 +648,146 @@ mod tests {
assert!(validate(&mut mock_hal, coin_params, &payment_request, value, address).is_ok());
}
+ #[cfg(feature = "app-litecoin")]
+ {
+ // BTC -> LTC swap
+ let source_keypath = [
+ 84 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 11 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ];
+ let source_address = super::super::derive_address_simple(
+ &mut mock_hal,
+ pb::BtcCoin::Btc,
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &source_keypath,
+ )
+ .unwrap();
+
+ let destination_keypath = [
+ 84 + util::bip32::HARDENED,
+ 2 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ];
+ let destination_address = super::super::derive_address_simple(
+ &mut mock_hal,
+ pb::BtcCoin::Ltc,
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &destination_keypath,
+ )
+ .unwrap();
+
+ let source_coin_params = params::get(pb::BtcCoin::Btc);
+
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 2,
+ "0.25 LTC",
+ &destination_address,
+ Some(dummy_btc_address_derivation(
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &destination_keypath,
+ )),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+
+ tst_sign_payment_request(
+ source_coin_params,
+ &mut payment_request,
+ value,
+ &source_address,
+ );
+
+ assert!(
+ validate(
+ &mut mock_hal,
+ source_coin_params,
+ &payment_request,
+ value,
+ &source_address,
+ )
+ .is_ok()
+ );
+ }
+
+ #[cfg(feature = "app-litecoin")]
+ {
+ // LTC -> BTC swap
+ let source_keypath = [
+ 84 + util::bip32::HARDENED,
+ 2 + util::bip32::HARDENED,
+ 11 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ];
+ let source_address = super::super::derive_address_simple(
+ &mut mock_hal,
+ pb::BtcCoin::Ltc,
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &source_keypath,
+ )
+ .unwrap();
+
+ let destination_keypath = [
+ 84 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ];
+ let destination_address = super::super::derive_address_simple(
+ &mut mock_hal,
+ pb::BtcCoin::Btc,
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &destination_keypath,
+ )
+ .unwrap();
+
+ let source_coin_params = params::get(pb::BtcCoin::Ltc);
+
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 0,
+ "0.25 BTC",
+ &destination_address,
+ Some(dummy_btc_address_derivation(
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &destination_keypath,
+ )),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+
+ tst_sign_payment_request(
+ source_coin_params,
+ &mut payment_request,
+ value,
+ &source_address,
+ );
+
+ assert!(
+ validate(
+ &mut mock_hal,
+ source_coin_params,
+ &payment_request,
+ value,
+ &source_address,
+ )
+ .is_ok()
+ );
+ }
+
// Unhappy cases:
#[cfg(feature = "app-ethereum")]
@@ -695,6 +911,177 @@ mod tests {
}
}
+ #[cfg(feature = "app-litecoin")]
+ {
+ // BTC-like destination keypath is valid, but claimed address does not match.
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 2,
+ "0.25 LTC",
+ "ltc1qwrongdestinationaddressthatdoesnotmatch4w7g4j",
+ Some(dummy_btc_address_derivation(
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &[
+ 84 + util::bip32::HARDENED,
+ 2 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ],
+ )),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
+ Err(ValidationError::AddressMismatch)
+ ));
+ }
+
+ #[cfg(feature = "app-ethereum")]
+ {
+ // BTC-like destinations only support BTC/LTC mainnet coin_type values.
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 1,
+ "0.25 BTC",
+ "bc1qanything",
+ Some(dummy_btc_address_derivation(
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &[
+ 84 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ],
+ )),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
+ Err(ValidationError::Other)
+ ));
+ }
+
+ #[cfg(feature = "app-litecoin")]
+ {
+ // BTC-like amount unit must agree with the destination coin_type.
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 2,
+ "0.25 BTC",
+ "bc1qanything",
+ Some(dummy_btc_address_derivation(
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &[
+ 84 + util::bip32::HARDENED,
+ 2 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ],
+ )),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
+ Err(ValidationError::Other)
+ ));
+ }
+
+ #[cfg(feature = "app-litecoin")]
+ {
+ // Invalid BTC keypath in CoinPurchaseMemo
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 0,
+ "0.25 BTC",
+ "bc1qanything",
+ Some(dummy_btc_address_derivation(
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &[
+ 84 + util::bip32::HARDENED,
+ 2 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ],
+ )),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
+ Err(ValidationError::Other)
+ ));
+ }
+
+ #[cfg(feature = "app-litecoin")]
+ {
+ // BTC-like destinations are simple singlesig only.
+ let mut payment_request = pb::BtcPaymentRequestRequest {
+ recipient_name: "Test Merchant".into(),
+ memos: vec![make_coin_purchase_memo(
+ 0,
+ "0.25 BTC",
+ "bc1qanything",
+ Some(memo::coin_purchase_memo::AddressDerivation::Btc(
+ memo::coin_purchase_memo::BtcAddressDerivation {
+ script_config: Some(pb::BtcScriptConfigWithKeypath {
+ script_config: Some(pb::BtcScriptConfig {
+ config: Some(pb::btc_script_config::Config::Multisig(
+ pb::btc_script_config::Multisig {
+ threshold: 1,
+ xpubs: vec![],
+ our_xpub_index: 0,
+ script_type:
+ pb::btc_script_config::multisig::ScriptType::P2wsh
+ as _,
+ },
+ )),
+ }),
+ keypath: vec![
+ 48 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 2 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ],
+ }),
+ },
+ )),
+ )],
+ nonce: vec![],
+ total_amount: value,
+ signature: vec![],
+ };
+ tst_sign_payment_request(coin_params, &mut payment_request, value, address);
+ assert!(matches!(
+ validate(&mut mock_hal, coin_params, &payment_request, value, address),
+ Err(ValidationError::Other)
+ ));
+ }
+
// Unknown recipient
let payment_request = pb::BtcPaymentRequestRequest {
recipient_name: "Unknown Merchant".into(),
@@ -838,7 +1225,61 @@ mod tests {
);
}
- #[cfg(feature = "app-ethereum")]
+ #[cfg(feature = "app-litecoin")]
+ #[test]
+ fn test_user_verify_swap_btc_destination() {
+ // BTC -> LTC swap
+ let mut mock_hal = TestingHal::new();
+ block_on(user_verify(
+ &mut mock_hal,
+ params::get(pb::BtcCoin::Btc),
+ &pb::BtcPaymentRequestRequest {
+ recipient_name: "SWAPKIT (Provider)".into(),
+ memos: vec![make_coin_purchase_memo(
+ 2,
+ "0.25 LTC",
+ "ltc1qdestination",
+ Some(dummy_btc_address_derivation(
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &[
+ 84 + util::bip32::HARDENED,
+ 2 + util::bip32::HARDENED,
+ 0 + util::bip32::HARDENED,
+ 0,
+ 0,
+ ],
+ )),
+ )],
+ nonce: vec![],
+ total_amount: 25000000,
+ signature: vec![],
+ },
+ FormatUnit::Default,
+ ))
+ .unwrap();
+
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![
+ Screen::Recipient {
+ recipient: "SWAPKIT (Provider)".into(),
+ amount: "0.25000000 BTC".into(),
+ },
+ Screen::Confirm {
+ title: "SWAP".into(),
+ body: "0.25000000 BTC\nto\n0.25 LTC".into(),
+ longtouch: false,
+ },
+ Screen::Confirm {
+ title: "Receive to".into(),
+ body: "LTC account #1".into(),
+ longtouch: false,
+ },
+ ]
+ );
+ }
+
+ #[cfg(all(feature = "app-litecoin", feature = "app-ethereum"))]
#[test]
fn test_user_verify_swap_invalid() {
// Invalid swap requests that user_verify must reject because the
@@ -846,7 +1287,7 @@ mod tests {
let coin_params = params::get(pb::BtcCoin::Btc);
for payment_request in [
- // Missing destination derivation, so "Send to" cannot be built.
+ // Missing destination derivation, so "Receive to" cannot be built.
pb::BtcPaymentRequestRequest {
recipient_name: "SWAPKIT (Provider)".into(),
memos: vec![make_coin_purchase_memo(60, "0.25 ETH", "0x123", None)],
@@ -871,20 +1312,16 @@ mod tests {
total_amount: 25000000,
signature: vec![],
},
- // Destination account element must be hardened.
+ // BTC-like derivation requires script_config.
pb::BtcPaymentRequestRequest {
recipient_name: "SWAPKIT (Provider)".into(),
memos: vec![make_coin_purchase_memo(
- 60,
- "0.25 ETH",
- "0x123",
- Some(memo::coin_purchase_memo::AddressDerivation::Eth(
- memo::coin_purchase_memo::EthAddressDerivation {
- keypath: vec![
- 44 + util::bip32::HARDENED,
- 60 + util::bip32::HARDENED,
- 0,
- ],
+ 0,
+ "0.25 BTC",
+ "bc1qdestination",
+ Some(memo::coin_purchase_memo::AddressDerivation::Btc(
+ memo::coin_purchase_memo::BtcAddressDerivation {
+ script_config: None,
},
)),
)],
@@ -892,14 +1329,17 @@ mod tests {
total_amount: 25000000,
signature: vec![],
},
- // Display amount must contain both numeric amount and destination unit.
+ // BTC-like destination keypath is too short to contain an account element.
pb::BtcPaymentRequestRequest {
recipient_name: "SWAPKIT (Provider)".into(),
memos: vec![make_coin_purchase_memo(
- 60,
- "ETH",
- "0x123",
- Some(dummy_eth_address_derivation(/*valid=*/ true)),
+ 0,
+ "0.25 BTC",
+ "bc1qdestination",
+ Some(dummy_btc_address_derivation(
+ pb::btc_script_config::SimpleType::P2wpkh,
+ &[84 + util::bip32::HARDENED, 0 + util::bip32::HARDENED],
+ )),
)],
nonce: vec![],
total_amount: 25000000,
diff --git a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
index ee4469f..e91f92d 100644
--- a/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
+++ b/src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
@@ -951,7 +951,7 @@ pub mod btc_payment_request_request {
/// Address to send the purchased coins to
#[prost(string, tag = "3")]
pub address: ::prost::alloc::string::String,
- #[prost(oneof = "coin_purchase_memo::AddressDerivation", tags = "4")]
+ #[prost(oneof = "coin_purchase_memo::AddressDerivation", tags = "4, 5")]
pub address_derivation: ::core::option::Option<
coin_purchase_memo::AddressDerivation,
>,
@@ -968,10 +968,21 @@ pub mod btc_payment_request_request {
pub keypath: ::prost::alloc::vec::Vec<u32>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
+ #[derive(Clone, PartialEq, ::prost::Message)]
+ pub struct BtcAddressDerivation {
+ /// Script config + keypath are needed to derive BTC/LTC-family addresses.
+ #[prost(message, optional, tag = "1")]
+ pub script_config: ::core::option::Option<
+ super::super::super::BtcScriptConfigWithKeypath,
+ >,
+ }
+ #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum AddressDerivation {
#[prost(message, tag = "4")]
Eth(EthAddressDerivation),
+ #[prost(message, tag = "5")]
+ Btc(BtcAddressDerivation),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
Why this scored 28/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.