Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bitcoin: support sending to silent payment addresses #1220

Merged
merged 2 commits into from
Sep 18, 2024

Conversation

benma
Copy link
Collaborator

@benma benma commented May 20, 2024

No description provided.

@benma
Copy link
Collaborator Author

benma commented May 20, 2024

This currently adds 9228 bytes to the binary, luckily less than this PR saves 😄 #1215

@benma
Copy link
Collaborator Author

benma commented Jun 9, 2024

Rebased. Need to double-check if payment requests to silent payment addresses work safely (an output that is both attached to a payment request and is generated by a SP address) and add unit tests for it, or if we should disable that combination.

Edit: I don't see a reason silent payment addresses could not safely be used within payment requests. The payment request signs for address strings, not pubkeyScripts, so it can simply contain the silent payment address.

@benma
Copy link
Collaborator Author

benma commented Aug 18, 2024

Rebased.

@benma
Copy link
Collaborator Author

benma commented Aug 22, 2024

Host side that verifies the generated output correctness using the DLEQ proof and returns them: BitBoxSwiss/bitbox02-api-go#105

This includes a simulator test the complete integration is tested.

@benma benma marked this pull request as ready for review August 27, 2024 08:51
@benma benma requested a review from NickeZ September 1, 2024 09:34
@benma
Copy link
Collaborator Author

benma commented Sep 1, 2024

@NickeZ one way to test it (except the integration/unit tests included in this PR) is an integration test on the host side with the simulator:

./scripts/docker_exec.sh make -j simulator

Then in the api-go client repo at BitBoxSwiss/bitbox02-api-go#105, run

SIMULATOR=/path/to/bitbox02-firmware/build-build/bin/simulator go test -v -run SilentPayment ./...
=== RUN   TestSimulatorBTCSignSilentPayment/simulator
--- PASS: TestSimulatorBTCSignSilentPayment (0.33s)
    --- PASS: TestSimulatorBTCSignSilentPayment/simulator (0.33s)

Copy link
Collaborator

@NickeZ NickeZ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general approved, left some minor comments.


const DLEQ_PROOF_SIZE: usize = 33 + 64;

pub struct Output {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps name it TransactionOutput since Output can be a bit generic

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}

impl Network {
fn sp_hrp(&self) -> &str {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrp is maybe a bit cryptic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment

SecretKey::from_slice(&hash).map_err(|_| ())
}

fn decode_address(address: &str, expected_hrp: &str) -> Result<(PublicKey, PublicKey), ()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider returning a struct so that you can name the fields. You can still do destructuring at the call site if you like. https://doc.rust-lang.org/rust-by-example/flow_control/match/destructuring/destructure_structures.html

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

fn decode_address(address: &str, expected_hrp: &str) -> Result<(PublicKey, PublicKey), ()> {
let mut decoded_addr =
bech32::primitives::decode::CheckedHrpstring::new::<bech32::Bech32m>(address)
.map_err(|_| ())?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For error handling, thiserror is a common crate for creating library level error enums. It has a convenient macro for wrapping dependencies errors as well. https://docs.rs/thiserror/latest/thiserror/

Another, simpler, option is to use &'static str as error type.

Throwing away errors with Err(()) feels a bit awkward. If you think it is programmer errors and not user errors, consider using .expect(reason) instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skpping for now

// - re-computing the output (https://github.com/bitcoin/bips/blob/ad1d3bc2a7b0d84247c29f847e85c35283094e2f/bip-0352.mediawiki#creating-outputs)
// using ecdsa_shared_secret = input_hash·a_sum·scan_pubkey = input_hash·c_pubkey
// - verifying that the created output matches the re-computed output.
fn create_dleq_proof<C: secp256k1::Verification>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to make your function non-generic and instead put Secp256k1<secp256k1::All> as the type in the argument list?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, worked.

}

let data: Vec<u8> = decoded_addr.byte_iter().collect();
if data.len() != 66 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should magic number 66 be a const?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added const PUBKEY_LEN: usize = 33;, and used 2 * PUBKEYLEN here.

if data.len() != 66 {
return Err(());
}
let scan_pubkey = PublicKey::from_slice(&data[..33]).map_err(|_| ())?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should magic number 33 be a const?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

input_type: InputType,
input_key: &SecretKey,
prevout: bitcoin::OutPoint,
) -> Result<(), ()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using an error type other than ().

@@ -303,6 +303,12 @@ USE_RESULT bool keystore_secp256k1_schnorr_bip86_sign(
const uint8_t* msg32,
uint8_t* sig64_out);

USE_RESULT bool keystore_secp256k1_get_private_key(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing doc comment for this function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

@@ -137,6 +138,9 @@ message BTCSignNextResponse {
// Previous tx's input/output index in case of PREV_INPUT or PREV_OUTPUT, for the input at `index`.
uint32 prev_index = 5;
AntiKleptoSignerCommitment anti_klepto_signer_commitment = 6;
// Generated output. The host *must* verify its correctness using silent_payment_dleq_proof.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you write silent_payment_dleq_proof (with ticks `) it will be nicer formatted in the generated docs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

BIP-352: https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki

A silent payment address contains two pubkeys, B_scan and
B_spend. From that, Taproot outputs can be created by using a ECDH
shared secret - see the BIP Overview section.

The created output is returned to the host along with a DLEQ proof the
host can use to independently verify the output was created
correctly. This mitigates bugs, potential memory
corruption (bit-flips), etc. in the firmware leading to catastrophic
failure.

The added vendored deps serde/serde_json etc. are dev-dependencies to
run the tests in the new crate, not used for firmware builds.

**A note about multiple silent payment outputs per transaction:**

In general, a transaction can send to an arbitrary amount of silent
payment addresses. If there are multiple, then for each unique
B_scan (multiple outputs to the same recipient/B_scan), a counter `k`
is incremented. See
https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#creating-outputs.

To be able to verify the silent payment address, we need to derive the
output and check that it matches the provided output. For each SP
output, we'd then need to know the SP address and the counter `k`.

The problem with this is that we also need to check, per B_scan, that
each `k=0, 1, ...` is used starting at zero with no holes. This
requires non-constant memory. We could still do it and support a
limited (but likely high) number of SP outputs per transaction, but
that complicates the code. For this reason, we restrict to only one SP
output per transaction.
@benma benma merged commit 83cd6c8 into BitBoxSwiss:master Sep 18, 2024
3 checks passed
@benma benma deleted the sp branch September 18, 2024 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants