Skip to content

Commit 2319b70

Browse files
authored
Merge pull request #257 from BitGo/BTC-0.fix-zcash-blake-bug
feat(wasm-utxo): fix zcash blake2b sighash for 256-byte outputs
2 parents bafffdc + dee233b commit 2319b70

3 files changed

Lines changed: 110 additions & 3 deletions

File tree

packages/wasm-utxo/Cargo.lock

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

packages/wasm-utxo/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ inspect = ["dep:num-bigint", "dep:serde", "dep:serde_json", "dep:hex"]
2727
[dependencies]
2828
wasm-bindgen = "0.2"
2929
js-sys = "0.3"
30-
miniscript = { git = "https://github.com/BitGo/rust-miniscript", tag = "miniscript-13.0.0-bitgo.1" }
30+
miniscript = { git = "https://github.com/BitGo/rust-miniscript", tag = "miniscript-13.0.0-bitgo.2" }
3131
bech32 = "0.11"
3232
musig2 = { version = "0.3.1", default-features = false, features = ["k256"] }
3333
getrandom = { version = "0.2", features = ["js"] }

packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5415,4 +5415,111 @@ mod tests {
54155415
// Result should be sorted back to [user, backup, bitgo]
54165416
assert_eq!(result.xpubs, xpubs);
54175417
}
5418+
5419+
/// Regression test: ZIP-243 sighash must be correct when the outputs preimage is exactly
5420+
/// 256 bytes (an exact multiple of the 128-byte BLAKE2b block size).
5421+
///
5422+
/// A transaction with 8 P2SH outputs produces a 256-byte outputs preimage:
5423+
/// each P2SH output serializes to 8 (value) + 1 (varint 23) + 23 (script) = 32 bytes,
5424+
/// so 8 × 32 = 256 = 2 × 128.
5425+
///
5426+
/// The old `blake2b_256_personal` implementation processed all complete blocks with the
5427+
/// finalization flag `f=0`, leaving an empty buffer for `finalize_variable_core`.
5428+
/// That caused a spurious all-zero block to be compressed as the final block, producing
5429+
/// the wrong `hashOutputs` and therefore a wrong sighash — making every signature over
5430+
/// such a transaction invalid.
5431+
///
5432+
/// This test uses a pre-computed signature from the fixed implementation as a test vector.
5433+
/// A sign-then-verify round-trip would pass even on the buggy version because both sides
5434+
/// would agree on the same (wrong) sighash. Verifying a pre-generated correct signature
5435+
/// fails on the buggy version because the verifier recomputes a different (wrong) sighash.
5436+
#[test]
5437+
fn test_zcash_sighash_block_aligned_outputs() {
5438+
use crate::fixed_script_wallet::test_utils::get_test_wallet_keys;
5439+
use crate::zcash::NetworkUpgrade;
5440+
use miniscript::bitcoin::ecdsa::Signature as EcdsaSig;
5441+
use miniscript::bitcoin::hashes::Hash;
5442+
use miniscript::bitcoin::secp256k1::Secp256k1;
5443+
use miniscript::bitcoin::{CompressedPublicKey, PublicKey, Txid};
5444+
5445+
let seed = "zcash_block_aligned";
5446+
let secp = Secp256k1::new();
5447+
5448+
let wallet_keys = RootWalletKeys::new(get_test_wallet_keys(seed));
5449+
5450+
let sapling_height = NetworkUpgrade::Sapling.testnet_activation_height();
5451+
let mut psbt = BitGoPsbt::new_zcash_at_height(
5452+
Network::ZcashTestnet,
5453+
&wallet_keys,
5454+
sapling_height,
5455+
None,
5456+
None,
5457+
None,
5458+
None,
5459+
)
5460+
.expect("new_zcash_at_height");
5461+
5462+
// 1 input spending a P2SH output
5463+
psbt.add_wallet_input(
5464+
Txid::all_zeros(),
5465+
0,
5466+
100_000,
5467+
&wallet_keys,
5468+
ScriptId { chain: 0, index: 0 },
5469+
WalletInputOptions::default(),
5470+
)
5471+
.expect("add_wallet_input");
5472+
5473+
// 8 P2SH outputs — each is 32 bytes (8 value + 1 varint + 23 script),
5474+
// so the outputs preimage is 8 × 32 = 256 bytes = 2 BLAKE2b blocks.
5475+
for i in 0..8u32 {
5476+
psbt.add_wallet_output(0, i, 10_000, &wallet_keys)
5477+
.expect("add_wallet_output");
5478+
}
5479+
5480+
// Pre-computed test vector: user key signature produced by the fixed implementation.
5481+
// Derived from seed "zcash_block_aligned", user key at m/0/0/0/0.
5482+
let sig_bytes = hex::decode(
5483+
"3045022100b480fdda99f4a7c5bf62374204e88a336c355b282470c156e2d632987ca0313a\
5484+
02203abd32d68fc95c56b27be9608b4cd34ad16a09b6854c35bf9d41989d67a46d9401",
5485+
)
5486+
.unwrap();
5487+
let pubkey_bytes =
5488+
hex::decode("039c8019dee0882c7625ad609a54d19fe9996941c0f73256fcabe1f699c9ff2816")
5489+
.unwrap();
5490+
5491+
let compressed = CompressedPublicKey::from_slice(&pubkey_bytes).expect("compressed pubkey");
5492+
let pubkey = PublicKey::new(compressed.0);
5493+
let sig = EcdsaSig::from_slice(&sig_bytes).expect("pre-computed sig");
5494+
5495+
// Insert the pre-computed signature into the PSBT instead of signing.
5496+
// This ensures verification uses the fixed implementation's sighash as the reference.
5497+
let BitGoPsbt::Zcash(ref mut zcash_psbt, _) = psbt else {
5498+
panic!("expected Zcash PSBT variant");
5499+
};
5500+
zcash_psbt.psbt.inputs[0].partial_sigs.insert(pubkey, sig);
5501+
5502+
let consensus_branch_id =
5503+
propkv::get_zec_consensus_branch_id(&zcash_psbt.psbt).expect("branch id");
5504+
let version_group_id = zcash_psbt
5505+
.version_group_id
5506+
.unwrap_or(zcash_psbt::ZCASH_SAPLING_VERSION_GROUP_ID);
5507+
let expiry_height = zcash_psbt.expiry_height.unwrap_or(0);
5508+
5509+
let ok = psbt_wallet_input::verify_ecdsa_signature_zcash(
5510+
&secp,
5511+
&zcash_psbt.psbt,
5512+
0,
5513+
compressed,
5514+
consensus_branch_id,
5515+
version_group_id,
5516+
expiry_height,
5517+
)
5518+
.expect("verify_ecdsa_signature_zcash");
5519+
5520+
assert!(
5521+
ok,
5522+
"Zcash signature over 256-byte (block-aligned) outputs preimage must verify"
5523+
);
5524+
}
54185525
}

0 commit comments

Comments
 (0)