Skip to content

Commit 3b91825

Browse files
committed
bitcoin: support sending to silent payment addresses
1 parent 39d761c commit 3b91825

File tree

13 files changed

+409
-100
lines changed

13 files changed

+409
-100
lines changed

CHANGELOG.md

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

99
### [Unreleased]
10+
- Bitcoin: add support for sending to silent payment (BIP-352) addresses
1011
- Bitcoin: add support for regtest
1112

1213
### 9.20.0

messages/btc.proto

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ message BTCSignInitRequest {
114114
SAT = 1;
115115
}
116116
FormatUnit format_unit = 8;
117+
bool contains_silent_payment_outputs = 9;
117118
}
118119

119120
message BTCSignNextResponse {
@@ -137,6 +138,9 @@ message BTCSignNextResponse {
137138
// Previous tx's input/output index in case of PREV_INPUT or PREV_OUTPUT, for the input at `index`.
138139
uint32 prev_index = 5;
139140
AntiKleptoSignerCommitment anti_klepto_signer_commitment = 6;
141+
// Generated output. The host *must* verify its correctness using `silent_payment_dleq_proof`.
142+
bytes generated_output_pkscript = 7;
143+
bytes silent_payment_dleq_proof = 8;
140144
}
141145

142146
message BTCSignInputRequest {
@@ -160,6 +164,10 @@ enum BTCOutputType {
160164
}
161165

162166
message BTCSignOutputRequest {
167+
// https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki
168+
message SilentPayment {
169+
string address = 1;
170+
}
163171
bool ours = 1;
164172
BTCOutputType type = 2; // if ours is false
165173
// 20 bytes for p2pkh, p2sh, pw2wpkh. 32 bytes for p2wsh.
@@ -169,6 +177,9 @@ message BTCSignOutputRequest {
169177
// If ours is true. References a script config from BTCSignInitRequest
170178
uint32 script_config_index = 6;
171179
optional uint32 payment_request_index = 7;
180+
// If provided, `type` and `payload` is ignored. The generated output pkScript is returned in
181+
// BTCSignNextResponse. `contains_silent_payment_outputs` in the init request must be true.
182+
SilentPayment silent_payment = 8;
172183
}
173184

174185
message BTCScriptConfigRegistration {

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

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

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ class BTCSignInitRequest(google.protobuf.message.Message):
292292
NUM_OUTPUTS_FIELD_NUMBER: builtins.int
293293
LOCKTIME_FIELD_NUMBER: builtins.int
294294
FORMAT_UNIT_FIELD_NUMBER: builtins.int
295+
CONTAINS_SILENT_PAYMENT_OUTPUTS_FIELD_NUMBER: builtins.int
295296
coin: global___BTCCoin.ValueType
296297
@property
297298
def script_configs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BTCScriptConfigWithKeypath]:
@@ -306,6 +307,7 @@ class BTCSignInitRequest(google.protobuf.message.Message):
306307
"""must be <500000000"""
307308

308309
format_unit: global___BTCSignInitRequest.FormatUnit.ValueType
310+
contains_silent_payment_outputs: builtins.bool
309311
def __init__(self,
310312
*,
311313
coin: global___BTCCoin.ValueType = ...,
@@ -315,8 +317,9 @@ class BTCSignInitRequest(google.protobuf.message.Message):
315317
num_outputs: builtins.int = ...,
316318
locktime: builtins.int = ...,
317319
format_unit: global___BTCSignInitRequest.FormatUnit.ValueType = ...,
320+
contains_silent_payment_outputs: builtins.bool = ...,
318321
) -> None: ...
319-
def ClearField(self, field_name: typing_extensions.Literal["coin",b"coin","format_unit",b"format_unit","locktime",b"locktime","num_inputs",b"num_inputs","num_outputs",b"num_outputs","script_configs",b"script_configs","version",b"version"]) -> None: ...
322+
def ClearField(self, field_name: typing_extensions.Literal["coin",b"coin","contains_silent_payment_outputs",b"contains_silent_payment_outputs","format_unit",b"format_unit","locktime",b"locktime","num_inputs",b"num_inputs","num_outputs",b"num_outputs","script_configs",b"script_configs","version",b"version"]) -> None: ...
320323
global___BTCSignInitRequest = BTCSignInitRequest
321324

322325
class BTCSignNextResponse(google.protobuf.message.Message):
@@ -356,6 +359,8 @@ class BTCSignNextResponse(google.protobuf.message.Message):
356359
SIGNATURE_FIELD_NUMBER: builtins.int
357360
PREV_INDEX_FIELD_NUMBER: builtins.int
358361
ANTI_KLEPTO_SIGNER_COMMITMENT_FIELD_NUMBER: builtins.int
362+
GENERATED_OUTPUT_PKSCRIPT_FIELD_NUMBER: builtins.int
363+
SILENT_PAYMENT_DLEQ_PROOF_FIELD_NUMBER: builtins.int
359364
type: global___BTCSignNextResponse.Type.ValueType
360365
index: builtins.int
361366
"""index of the current input or output"""
@@ -371,6 +376,10 @@ class BTCSignNextResponse(google.protobuf.message.Message):
371376

372377
@property
373378
def anti_klepto_signer_commitment(self) -> antiklepto_pb2.AntiKleptoSignerCommitment: ...
379+
generated_output_pkscript: builtins.bytes
380+
"""Generated output. The host *must* verify its correctness using `silent_payment_dleq_proof`."""
381+
382+
silent_payment_dleq_proof: builtins.bytes
374383
def __init__(self,
375384
*,
376385
type: global___BTCSignNextResponse.Type.ValueType = ...,
@@ -379,9 +388,11 @@ class BTCSignNextResponse(google.protobuf.message.Message):
379388
signature: builtins.bytes = ...,
380389
prev_index: builtins.int = ...,
381390
anti_klepto_signer_commitment: typing.Optional[antiklepto_pb2.AntiKleptoSignerCommitment] = ...,
391+
generated_output_pkscript: builtins.bytes = ...,
392+
silent_payment_dleq_proof: builtins.bytes = ...,
382393
) -> None: ...
383394
def HasField(self, field_name: typing_extensions.Literal["anti_klepto_signer_commitment",b"anti_klepto_signer_commitment"]) -> builtins.bool: ...
384-
def ClearField(self, field_name: typing_extensions.Literal["anti_klepto_signer_commitment",b"anti_klepto_signer_commitment","has_signature",b"has_signature","index",b"index","prev_index",b"prev_index","signature",b"signature","type",b"type"]) -> None: ...
395+
def ClearField(self, field_name: typing_extensions.Literal["anti_klepto_signer_commitment",b"anti_klepto_signer_commitment","generated_output_pkscript",b"generated_output_pkscript","has_signature",b"has_signature","index",b"index","prev_index",b"prev_index","signature",b"signature","silent_payment_dleq_proof",b"silent_payment_dleq_proof","type",b"type"]) -> None: ...
385396
global___BTCSignNextResponse = BTCSignNextResponse
386397

387398
class BTCSignInputRequest(google.protobuf.message.Message):
@@ -424,13 +435,25 @@ global___BTCSignInputRequest = BTCSignInputRequest
424435

425436
class BTCSignOutputRequest(google.protobuf.message.Message):
426437
DESCRIPTOR: google.protobuf.descriptor.Descriptor
438+
class SilentPayment(google.protobuf.message.Message):
439+
"""https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki"""
440+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
441+
ADDRESS_FIELD_NUMBER: builtins.int
442+
address: typing.Text
443+
def __init__(self,
444+
*,
445+
address: typing.Text = ...,
446+
) -> None: ...
447+
def ClearField(self, field_name: typing_extensions.Literal["address",b"address"]) -> None: ...
448+
427449
OURS_FIELD_NUMBER: builtins.int
428450
TYPE_FIELD_NUMBER: builtins.int
429451
VALUE_FIELD_NUMBER: builtins.int
430452
PAYLOAD_FIELD_NUMBER: builtins.int
431453
KEYPATH_FIELD_NUMBER: builtins.int
432454
SCRIPT_CONFIG_INDEX_FIELD_NUMBER: builtins.int
433455
PAYMENT_REQUEST_INDEX_FIELD_NUMBER: builtins.int
456+
SILENT_PAYMENT_FIELD_NUMBER: builtins.int
434457
ours: builtins.bool
435458
type: global___BTCOutputType.ValueType
436459
"""if ours is false"""
@@ -449,6 +472,12 @@ class BTCSignOutputRequest(google.protobuf.message.Message):
449472
"""If ours is true. References a script config from BTCSignInitRequest"""
450473

451474
payment_request_index: builtins.int
475+
@property
476+
def silent_payment(self) -> global___BTCSignOutputRequest.SilentPayment:
477+
"""If provided, `type` and `payload` is ignored. The generated output pkScript is returned in
478+
BTCSignNextResponse. `contains_silent_payment_outputs` in the init request must be true.
479+
"""
480+
pass
452481
def __init__(self,
453482
*,
454483
ours: builtins.bool = ...,
@@ -458,9 +487,10 @@ class BTCSignOutputRequest(google.protobuf.message.Message):
458487
keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
459488
script_config_index: builtins.int = ...,
460489
payment_request_index: typing.Optional[builtins.int] = ...,
490+
silent_payment: typing.Optional[global___BTCSignOutputRequest.SilentPayment] = ...,
461491
) -> None: ...
462-
def HasField(self, field_name: typing_extensions.Literal["_payment_request_index",b"_payment_request_index","payment_request_index",b"payment_request_index"]) -> builtins.bool: ...
463-
def ClearField(self, field_name: typing_extensions.Literal["_payment_request_index",b"_payment_request_index","keypath",b"keypath","ours",b"ours","payload",b"payload","payment_request_index",b"payment_request_index","script_config_index",b"script_config_index","type",b"type","value",b"value"]) -> None: ...
492+
def HasField(self, field_name: typing_extensions.Literal["_payment_request_index",b"_payment_request_index","payment_request_index",b"payment_request_index","silent_payment",b"silent_payment"]) -> builtins.bool: ...
493+
def ClearField(self, field_name: typing_extensions.Literal["_payment_request_index",b"_payment_request_index","keypath",b"keypath","ours",b"ours","payload",b"payload","payment_request_index",b"payment_request_index","script_config_index",b"script_config_index","silent_payment",b"silent_payment","type",b"type","value",b"value"]) -> None: ...
464494
def WhichOneof(self, oneof_group: typing_extensions.Literal["_payment_request_index",b"_payment_request_index"]) -> typing.Optional[typing_extensions.Literal["payment_request_index"]]: ...
465495
global___BTCSignOutputRequest = BTCSignOutputRequest
466496

src/keystore.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,29 @@ bool keystore_secp256k1_schnorr_bip86_sign(
10001000
return secp256k1_schnorrsig_verify(ctx, sig64_out, msg32, 32, &pubkey) == 1;
10011001
}
10021002

1003+
bool keystore_secp256k1_get_private_key(
1004+
const uint32_t* keypath,
1005+
const size_t keypath_len,
1006+
bool tweak_bip86,
1007+
uint8_t* key_out)
1008+
{
1009+
if (tweak_bip86) {
1010+
secp256k1_keypair __attribute__((__cleanup__(_cleanup_keypair))) keypair = {0};
1011+
secp256k1_xonly_pubkey pubkey = {0};
1012+
if (!_schnorr_bip86_keypair(keypath, keypath_len, &keypair, &pubkey)) {
1013+
return false;
1014+
}
1015+
const secp256k1_context* ctx = wally_get_secp_context();
1016+
return secp256k1_keypair_sec(ctx, key_out, &keypair) == 1;
1017+
}
1018+
struct ext_key xprv __attribute__((__cleanup__(keystore_zero_xkey))) = {0};
1019+
if (!_get_xprv_twice(keypath, keypath_len, &xprv)) {
1020+
return false;
1021+
}
1022+
memcpy(key_out, xprv.priv_key + 1, 32);
1023+
return true;
1024+
}
1025+
10031026
#ifdef TESTING
10041027
void keystore_mock_unlocked(const uint8_t* seed, size_t seed_len, const uint8_t* bip39_seed)
10051028
{

src/keystore.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,20 @@ USE_RESULT bool keystore_secp256k1_schnorr_bip86_sign(
303303
const uint8_t* msg32,
304304
uint8_t* sig64_out);
305305

306+
/**
307+
* Get the private key at the keypath.
308+
*
309+
* @param[in] keypath derivation keypath
310+
* @param[in] keypath_len number of elements in keypath
311+
* @param[in] tweak_bip86 if true, the resulting private key is tweaked with the BIP-86 tweak.
312+
* @param[out] key_out resulting private key, must be 32 bytes.
313+
*/
314+
USE_RESULT bool keystore_secp256k1_get_private_key(
315+
const uint32_t* keypath,
316+
size_t keypath_len,
317+
bool tweak_bip86,
318+
uint8_t* key_out);
319+
306320
#ifdef TESTING
307321
/**
308322
* convenience to mock the keystore state (locked, seed) in tests.

src/rust/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/bitbox02-rust/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ util = { path = "../util" }
3232
erc20_params = { path = "../erc20_params", optional = true }
3333
binascii = { version = "0.1.4", default-features = false, features = ["encode"] }
3434
bitbox02-noise = {path = "../bitbox02-noise"}
35+
streaming-silent-payments = { path = "../streaming-silent-payments", optional = true }
3536
hex = { workspace = true }
3637
sha2 = { workspace = true }
3738
sha3 = { workspace = true, optional = true }
@@ -77,6 +78,7 @@ app-ethereum = [
7778
app-bitcoin = [
7879
"dep:bech32",
7980
"dep:miniscript",
81+
"dep:streaming-silent-payments",
8082
"bitbox02/app-bitcoin",
8183
]
8284
app-litecoin = [

0 commit comments

Comments
 (0)