Skip to content

send: false bad tweak when intermediate key sum hits zero #55

Description

@shuv-amp

I believe bdk_sp::send::create_silentpayment_partial_secret may fail when the running partial sum of eligible input keys hits zero, even though the final sum of all eligible keys is non-zero.

I first noticed this while differential-testing Silent Payments implementations, then reduced it to the standalone repro below against current bdk-sp HEAD.

The minimal key set is [A, -A, A] modulo secp256k1 order, so the final sum is A, not zero.

Concrete scalars:

A        = a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb
-A mod n = 592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456

To Reproduce
Create a temporary Cargo project with:

[package]
name = "upstream_repro_bdk_sp_head"
version = "0.1.0"
edition = "2024"

[dependencies]
bdk_sp = { git = "https://github.com/bitcoindevkit/bdk-sp.git", rev = "2f28d19581202d46fd0b30c35b6ae1cc45e37ce5" }
bitcoin = "0.32.8"
hex = "0.4"

and:

use bdk_sp::send::create_silentpayment_partial_secret;
use bitcoin::secp256k1::SecretKey;
use bitcoin::ScriptBuf;
use hex::encode;

fn smallest_outpoint_bytes(txid_hex: &str, vout: u32) -> [u8; 36] {
    let mut txid = hex::decode(txid_hex).unwrap();
    txid.reverse();
    let mut out = [0u8; 36];
    out[..32].copy_from_slice(&txid);
    out[32..].copy_from_slice(&vout.to_le_bytes());
    out
}

fn run_case(name: &str, keys: Vec<(ScriptBuf, SecretKey)>) {
    let txid = "3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e";
    match create_silentpayment_partial_secret(&smallest_outpoint_bytes(txid, 0), &keys) {
        Ok(secret) => println!("{name}: OK {}", encode(secret.secret_bytes())),
        Err(err) => println!("{name}: ERR {err}"),
    }
}

fn main() {
    let spk = ScriptBuf::from_bytes(
        hex::decode("00149d9e24f9fab4e35bf1a6df4b46cb533296ac0792").unwrap(),
    );
    let a = SecretKey::from_slice(
        &hex::decode("a6df6a0bb448992a301df4258e06a89fe7cf7146f59ac3bd5ff26083acb22ceb")
            .unwrap(),
    )
    .unwrap();
    let minus_a = SecretKey::from_slice(
        &hex::decode("592095f44bb766d5cfe20bda71f9575ed2df6b9fb9addc7e5fdffe0923841456")
            .unwrap(),
    )
    .unwrap();

    run_case(
        "[A,-A,A]",
        vec![(spk.clone(), a), (spk.clone(), minus_a), (spk.clone(), a)],
    );
    run_case(
        "[A,A,-A]",
        vec![(spk.clone(), a), (spk.clone(), a), (spk, minus_a)],
    );
}

Run:

cargo run --quiet

I get:

[A,-A,A]: ERR Silent payment sending error: bad tweak
[A,A,-A]: OK 9236293a30e156add5e1303da4c5867e191746e2971bedd8790065e587b08e49

Expected behavior
I would expect both orders to succeed.

Per BIP352 sender construction, the sender should sum all eligible input private keys first and fail only if the final sum is zero. In this case:

A + (-A) + A = A
A + A + (-A) = A

Both are non-zero, so the result should not depend on input order.

This reduced low-level repro came from a valid three-input transaction with three distinct eligible P2WPKH inputs sharing the same smallest outpoint 3a286147b25e16ae80aff406f2673c6e565418c40f45c071245cdebc8a94174e:0.

Build environment

  • Tag/commit: 2f28d19581202d46fd0b30c35b6ae1cc45e37ce5
  • OS+version: macOS 26.4
  • Rust/Cargo version: cargo 1.92.0, rustc 1.92.0
  • Rust/Cargo target: aarch64-apple-darwin

Additional context
The likely cause seems to be the left-to-right accumulation in silentpayments/src/send/mod.rs:

let mut a_sum = available_keys[0];
for sk in available_keys.iter().skip(1) {
    a_sum = a_sum.add_tweak(&Scalar::from(*sk))?;
}

If the intermediate sum reaches zero after A + (-A), add_tweak errors immediately, even though the remaining + A would make the final sum non-zero again.

The receive side appears to aggregate input public keys as a full set before hashing, so the send/receive asymmetry also seems suspicious.

If repeated key material across distinct eligible inputs is intentionally unsupported here, please ignore this and I’d appreciate a pointer to that restriction.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions