Skip to content

Commit 0a50a7f

Browse files
fix(crypto): fix BLAKE2b finalization for block-aligned inputs in ZIP-243
The blake2b_256_personal function had a manual loop that processed all complete 128-byte blocks with the finalization flag f=0. For inputs whose length is an exact multiple of the block size (e.g. the outputs preimage for 8+ outputs), the finalize call would find an empty buffer and compress a spurious all-zero block as the final block, producing the wrong hash. Fix: remove the manual loop and feed all data through the Lazy buffer (buffer.digest_blocks), which retains the last block until finalize so the correct finalization flag is always applied.
1 parent 8d7f0c5 commit 0a50a7f

1 file changed

Lines changed: 50 additions & 16 deletions

File tree

bitcoin/src/crypto/sighash_zcash.rs

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ use core::borrow::Borrow;
1212
use core::fmt;
1313

1414
use blake2::digest::core_api::{Buffer, UpdateCore};
15-
use blake2::digest::generic_array::GenericArray;
16-
use blake2::digest::typenum::U128;
1715
use blake2::digest::Output;
1816
use blake2::Blake2bVarCore;
1917

@@ -339,22 +337,12 @@ fn zcash_hash_single_output(tx: &Transaction, index: usize) -> [u8; 32] {
339337

340338
/// Compute BLAKE2b-256 hash with personalization for Zcash (ZIP-243).
341339
pub(crate) fn blake2b_256_personal(data: &[u8], personalization: &[u8]) -> [u8; 32] {
342-
// Create a new core with personalization. Parameters: (salt, persona, key_size, output_size)
343340
let mut core = Blake2bVarCore::new_with_params(&[], personalization, 0, 32);
344-
345-
// Process data in 128-byte blocks (BLAKE2b block size)
346-
let block_size = 128;
347-
let mut pos = 0;
348-
349-
while pos + block_size <= data.len() {
350-
let block = GenericArray::<u8, U128>::from_slice(&data[pos..pos + block_size]);
351-
core.update_blocks(core::slice::from_ref(block));
352-
pos += block_size;
353-
}
354-
355-
// Handle final block with padding
341+
// Use the Lazy buffer for all data so the last block is retained in the buffer
342+
// until finalize, ensuring the correct finalization flag even when data.len()
343+
// is an exact multiple of the 128-byte BLAKE2b block size.
356344
let mut buffer: Buffer<Blake2bVarCore> = Default::default();
357-
buffer.digest_blocks(&data[pos..], |blocks| core.update_blocks(blocks));
345+
buffer.digest_blocks(data, |blocks| core.update_blocks(blocks));
358346

359347
// Finalize
360348
let mut full_output: Output<Blake2bVarCore> = Default::default();
@@ -638,4 +626,50 @@ mod tests {
638626
.unwrap();
639627
assert_eq!(hash_outputs, expected_outputs, "hashOutputs mismatch");
640628
}
629+
630+
/// Regression test: blake2b_256_personal must produce the correct hash when the
631+
/// input length is an exact multiple of the 128-byte BLAKE2b block size.
632+
///
633+
/// The old implementation fed all complete blocks to `update_blocks` and left the
634+
/// buffer empty for `finalize_variable_core`, which then compressed a spurious
635+
/// all-zero block — producing a wrong hash for any block-aligned input.
636+
#[test]
637+
fn test_blake2b_256_personal_block_aligned() {
638+
// Expected values computed with Python hashlib:
639+
// hashlib.blake2b(data, digest_size=32, person=b"ZcashOutputsHash").hexdigest()
640+
let persona = b"ZcashOutputsHash";
641+
642+
// 256 bytes = 2 × block size
643+
let data256 = vec![0xabu8; 256];
644+
let expected256: [u8; 32] =
645+
FromHex::from_hex("f2cee55bab0bc6b421a97e26b7c55f63f22fea6cf5fbc5ad1c290872bd470f3e")
646+
.unwrap();
647+
assert_eq!(
648+
blake2b_256_personal(&data256, persona),
649+
expected256,
650+
"wrong hash for 256-byte (2-block) input"
651+
);
652+
653+
// 128 bytes = 1 × block size
654+
let data128 = vec![0xabu8; 128];
655+
let expected128: [u8; 32] =
656+
FromHex::from_hex("8e802425ab1d83222d0bcf18140d61ae70670796be480fdd50b3027a3ca5478d")
657+
.unwrap();
658+
assert_eq!(
659+
blake2b_256_personal(&data128, persona),
660+
expected128,
661+
"wrong hash for 128-byte (1-block) input"
662+
);
663+
664+
// 100 bytes (non-aligned) — sanity check that the fix didn't break the common case
665+
let data100 = vec![0xabu8; 100];
666+
let expected100: [u8; 32] =
667+
FromHex::from_hex("3df66bd2c00b813fff6119fde7464294eab4fbecc311dd18c2bddeb6120d97f7")
668+
.unwrap();
669+
assert_eq!(
670+
blake2b_256_personal(&data100, persona),
671+
expected100,
672+
"wrong hash for 100-byte (non-aligned) input"
673+
);
674+
}
641675
}

0 commit comments

Comments
 (0)