From 95b32e5f8295a4e6755bd9eee057da792ed1a862 Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Fri, 6 Feb 2026 09:16:49 +0100 Subject: [PATCH 1/3] Fix backward compatibility when building Bundle --- src/builder.rs | 50 +++++++++++++++++++++++++++++++++++++-- src/bundle/commitments.rs | 12 ++++++---- tests/builder.rs | 16 ++++++------- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index acb0665bf..2f6824016 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -969,7 +969,51 @@ fn build_bundle( // as we can estimate the vector size beforehand. let mut indexed_spends_outputs = Vec::with_capacity(num_actions); - let spends_outputs_by_asset = partition_by_asset(&spends, &outputs); + let mut spends_outputs_by_asset = partition_by_asset(&spends, &outputs); + + // To ensure backward compatibility, if + // - both spends and outputs are empty, or + // - only zatoshi assets are present and there are not enough spends or outputs, + // we add dummy spends and outputs until the minimum number of actions is reached. + // Dummy spends and outputs are added before shuffling. + { + if num_actions > 0 { + if spends_outputs_by_asset.is_empty() { + spends_outputs_by_asset.insert( + AssetBase::zatoshi(), + ( + vec![(SpendInfo::dummy(AssetBase::zatoshi(), &mut rng), None)], + vec![], + ), + ); + } + if spends_outputs_by_asset.len() == 1 { + // All spends and outputs have the same asset. + if let Some((spends, outputs)) = + spends_outputs_by_asset.get_mut(&AssetBase::zatoshi()) + { + // This asset is the zatoshi asset. + // So, we have to add dummy spends and outputs until the minimum number of actions is reached. + let pad_spends = num_actions.saturating_sub(spends.len()); + let pad_outputs = num_actions.saturating_sub(outputs.len()); + + spends.extend( + iter::repeat_with(|| { + (SpendInfo::dummy(AssetBase::zatoshi(), &mut rng), None) + }) + .take(pad_spends), + ); + outputs.extend( + iter::repeat_with(|| { + (OutputInfo::dummy(&mut rng, AssetBase::zatoshi()), None) + }) + .take(pad_outputs), + ); + } + } + } + } + let asset_count = spends_outputs_by_asset.len(); indexed_spends_outputs.extend(spends_outputs_by_asset.into_iter().flat_map( |(asset, (spends, outputs))| { @@ -1021,7 +1065,9 @@ fn build_bundle( // We shuffled the spends and outputs within each `AssetBase` above; now we // shuffle the actions to achieve a similar property across `AssetBase`s. - indexed_spends_outputs.shuffle(&mut rng); + if asset_count > 1 { + indexed_spends_outputs.shuffle(&mut rng); + } let mut bundle_meta = BundleMetadata::new(num_requested_spends, num_requested_outputs); let pre_actions = indexed_spends_outputs diff --git a/src/bundle/commitments.rs b/src/bundle/commitments.rs index aca7d2198..dfce7db6b 100644 --- a/src/bundle/commitments.rs +++ b/src/bundle/commitments.rs @@ -168,7 +168,9 @@ mod tests { let sighash = hash_bundle_txid_data(&bundle); assert_eq!( sighash.to_hex().as_str(), - "f3ea89ea2b1e17b3313a6f2f9e4e47c21eec1574902f5ea6961227e1eaed2327" + // Bundle hash for Orchard (vanilla) generated using + // Zcash/Orchard commit: 4ac248d0 (v0.11.0) + "0ac1e319f6761a8561b7bd3fc0907a5c73ed5590a6c210c4d39ffae1d5741875" ); } @@ -184,7 +186,7 @@ mod tests { let sighash = hash_bundle_txid_data(&bundle); assert_eq!( sighash.to_hex().as_str(), - "a0d843b7278788e3b47dc9fe1e1da227a94898b7111d76514a87df486d32773c" + "f84871d872081fa7744cbaf575e342cf81951a9b17818264170243d1551a99ea" ); } @@ -213,7 +215,9 @@ mod tests { let orchard_auth_digest = hash_bundle_auth_data(&bundle, test_sighash_info_for_kind); assert_eq!( orchard_auth_digest.to_hex().as_str(), - "c99aa5a33fd4e7b78de0ee846397e2eb0da3a5d176e6df57d0401c49f51d7295" + // Bundle hash for Orchard (vanilla) generated using + // Zcash/Orchard commit: 4ac248d0 (v0.11.0) + "5f3bcf759cddf19170ec47a882a470b5767d66c95fc72ffc360f31324474a06b" ); } @@ -230,7 +234,7 @@ mod tests { let orchard_auth_digest = hash_bundle_auth_data(&bundle, test_sighash_info_for_kind); assert_eq!( orchard_auth_digest.to_hex().as_str(), - "9d47819082f2323b30ceabe0fea993b39541cc0e62a8be6e1bc2a19840b0d9ab" + "0c29408a07863016f5b4c5c0ccc5b944f24c686d06035945c5514f8b8c195a99" ); } diff --git a/tests/builder.rs b/tests/builder.rs index ba4c350aa..b563594de 100644 --- a/tests/builder.rs +++ b/tests/builder.rs @@ -213,16 +213,16 @@ fn bundle_chain_vanilla() { orchard_digest_1, // Locks the `orchard_digest` for OrchardVanilla [ - 25, 143, 25, 148, 146, 133, 196, 243, 163, 122, 136, 217, 179, 122, 70, 233, 4, 4, 26, - 170, 152, 243, 177, 199, 226, 241, 63, 143, 104, 77, 149, 254 + 165, 242, 106, 135, 168, 224, 110, 252, 175, 110, 63, 29, 78, 243, 33, 14, 152, 202, + 209, 47, 68, 32, 138, 96, 79, 213, 218, 93, 45, 87, 221, 174 ] ); assert_eq!( orchard_digest_2, // Locks the `orchard_digest` for OrchardVanilla [ - 164, 197, 26, 212, 108, 232, 219, 47, 64, 35, 3, 171, 77, 191, 253, 173, 173, 0, 148, - 119, 98, 210, 134, 196, 201, 205, 117, 10, 37, 72, 234, 3 + 74, 174, 42, 41, 68, 92, 171, 110, 10, 148, 217, 61, 68, 50, 49, 1, 1, 180, 221, 210, + 97, 237, 25, 198, 195, 77, 19, 160, 186, 172, 8, 26 ] ); } @@ -234,16 +234,16 @@ fn bundle_chain_zsa() { orchard_digest_1, // Locks the `orchard_digest` for OrchardZSA [ - 47, 247, 30, 9, 58, 47, 181, 208, 48, 162, 133, 51, 186, 54, 13, 82, 207, 227, 33, 48, - 223, 31, 90, 129, 96, 166, 247, 156, 122, 125, 100, 190 + 176, 24, 152, 89, 60, 222, 215, 240, 176, 197, 147, 81, 4, 84, 61, 189, 163, 117, 43, + 201, 63, 140, 116, 211, 133, 186, 54, 58, 171, 124, 192, 215 ] ); assert_eq!( orchard_digest_2, // Locks the `orchard_digest` for OrchardZSA [ - 40, 249, 161, 168, 11, 100, 205, 146, 11, 203, 210, 239, 51, 73, 208, 236, 47, 110, 49, - 18, 132, 199, 179, 63, 140, 28, 106, 34, 155, 93, 111, 254 + 161, 158, 107, 122, 89, 77, 236, 178, 130, 85, 148, 101, 237, 1, 67, 119, 76, 126, 233, + 123, 94, 240, 183, 227, 9, 245, 74, 51, 16, 12, 157, 60 ] ); } From e63a423e5ed8c98da048b247a53b8e8d20693921 Mon Sep 17 00:00:00 2001 From: Constance Beguier Date: Fri, 6 Feb 2026 10:00:13 +0100 Subject: [PATCH 2/3] Update zcash/orchard reference commit --- src/bundle/commitments.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bundle/commitments.rs b/src/bundle/commitments.rs index dfce7db6b..549b1402a 100644 --- a/src/bundle/commitments.rs +++ b/src/bundle/commitments.rs @@ -169,7 +169,7 @@ mod tests { assert_eq!( sighash.to_hex().as_str(), // Bundle hash for Orchard (vanilla) generated using - // Zcash/Orchard commit: 4ac248d0 (v0.11.0) + // Zcash/Orchard commit: 9d89b504 "0ac1e319f6761a8561b7bd3fc0907a5c73ed5590a6c210c4d39ffae1d5741875" ); } @@ -216,7 +216,7 @@ mod tests { assert_eq!( orchard_auth_digest.to_hex().as_str(), // Bundle hash for Orchard (vanilla) generated using - // Zcash/Orchard commit: 4ac248d0 (v0.11.0) + // Zcash/Orchard commit: 9d89b504 "5f3bcf759cddf19170ec47a882a470b5767d66c95fc72ffc360f31324474a06b" ); } From 1fc885613d615bb0b1634471b607ad4d41d763df Mon Sep 17 00:00:00 2001 From: Paul <3682187+PaulLaux@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:43:24 +0400 Subject: [PATCH 3/3] compact zatoshi-only padding --- src/builder.rs | 60 +++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 2f6824016..9b51655d1 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -971,47 +971,24 @@ fn build_bundle( let mut spends_outputs_by_asset = partition_by_asset(&spends, &outputs); - // To ensure backward compatibility, if - // - both spends and outputs are empty, or - // - only zatoshi assets are present and there are not enough spends or outputs, - // we add dummy spends and outputs until the minimum number of actions is reached. - // Dummy spends and outputs are added before shuffling. + // For zatoshi-only bundles, pad spends and outputs to num_actions + // before per-asset processing, so that dummies are created before the shuffle — + // matching vanilla Orchard RNG consumption order. + if spends_outputs_by_asset + .keys() + .all(|asset| asset == &AssetBase::zatoshi()) { - if num_actions > 0 { - if spends_outputs_by_asset.is_empty() { - spends_outputs_by_asset.insert( - AssetBase::zatoshi(), - ( - vec![(SpendInfo::dummy(AssetBase::zatoshi(), &mut rng), None)], - vec![], - ), - ); - } - if spends_outputs_by_asset.len() == 1 { - // All spends and outputs have the same asset. - if let Some((spends, outputs)) = - spends_outputs_by_asset.get_mut(&AssetBase::zatoshi()) - { - // This asset is the zatoshi asset. - // So, we have to add dummy spends and outputs until the minimum number of actions is reached. - let pad_spends = num_actions.saturating_sub(spends.len()); - let pad_outputs = num_actions.saturating_sub(outputs.len()); - - spends.extend( - iter::repeat_with(|| { - (SpendInfo::dummy(AssetBase::zatoshi(), &mut rng), None) - }) - .take(pad_spends), - ); - outputs.extend( - iter::repeat_with(|| { - (OutputInfo::dummy(&mut rng, AssetBase::zatoshi()), None) - }) - .take(pad_outputs), - ); - } - } - } + let (asset_spends, asset_outputs) = spends_outputs_by_asset + .entry(AssetBase::zatoshi()) + .or_insert_with(|| (vec![], vec![])); + asset_spends.extend( + iter::repeat_with(|| (SpendInfo::dummy(AssetBase::zatoshi(), &mut rng), None)) + .take(num_actions.saturating_sub(asset_spends.len())), + ); + asset_outputs.extend( + iter::repeat_with(|| (OutputInfo::dummy(&mut rng, AssetBase::zatoshi()), None)) + .take(num_actions.saturating_sub(asset_outputs.len())), + ); } let asset_count = spends_outputs_by_asset.len(); @@ -1053,6 +1030,9 @@ fn build_bundle( }, )); + // Pad total actions to num_actions. + // This covers the edge case of a single non-zatoshi asset with fewer than + // MIN_ACTIONS spends/outputs (e.g. a bundle that only burns a custom asset). indexed_spends_outputs.extend( iter::repeat_with(|| { (