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:
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.
I believe
bdk_sp::send::create_silentpayment_partial_secretmay 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-spHEAD.The minimal key set is
[A, -A, A]modulo secp256k1 order, so the final sum isA, not zero.Concrete scalars:
To Reproduce
Create a temporary Cargo project with:
and:
Run:
I get:
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:
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
2f28d19581202d46fd0b30c35b6ae1cc45e37ce5macOS 26.4cargo 1.92.0,rustc 1.92.0aarch64-apple-darwinAdditional context
The likely cause seems to be the left-to-right accumulation in
silentpayments/src/send/mod.rs:If the intermediate sum reaches zero after
A + (-A),add_tweakerrors immediately, even though the remaining+ Awould 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.