Skip to content

Commit 0bf444b

Browse files
committed
Merge branch 'sync-zcash-v4.1.0-merge-1' into sync-zcash-v4.1.0-merge
2 parents 426dd85 + 89d593d commit 0bf444b

File tree

10 files changed

+80
-54
lines changed

10 files changed

+80
-54
lines changed

zebra-chain/src/orchard/arbitrary.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,15 @@ impl Arbitrary for Flags {
126126
type Parameters = ();
127127

128128
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
129-
(any::<u8>()).prop_map(Self::from_bits_truncate).boxed()
129+
(any::<u8>())
130+
.prop_map(|byte| {
131+
// Clear ENABLE_ZSA: it is only allowed in V6, and this generator is
132+
// also used for V5 cases where the flag would make deserialization fail.
133+
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
134+
let byte = byte & !(Flags::ENABLE_ZSA.bits());
135+
Self::from_bits_truncate(byte)
136+
})
137+
.boxed()
130138
}
131139

132140
type Strategy = BoxedStrategy<Self>;

zebra-chain/src/orchard/shielded_data.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ bitflags! {
291291
/// # Consensus
292292
///
293293
/// > [NU5 onward] In a version 5 transaction, the reserved bits 2..7 of the flagsOrchard
294-
/// > field MUST be zero.
294+
/// > field MUST be zero. Bit 2 (ENABLE_ZSA) is introduced in V6 (NU7, ZIP 230).
295295
///
296296
/// <https://zips.z.cash/protocol/protocol.pdf#txnconsensus>
297297
///
@@ -306,7 +306,7 @@ bitflags! {
306306
/// Enable creating new non-zero valued Orchard notes.
307307
const ENABLE_OUTPUTS = 0b00000010;
308308
/// Enable ZSA transaction (otherwise all notes within actions must use native asset).
309-
// FIXME: Should we use this flag explicitly anywhere in Zebra?
309+
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
310310
const ENABLE_ZSA = 0b00000100;
311311
}
312312
}

zebra-chain/src/orchard_zsa/asset_state.rs

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,6 @@ impl AssetState {
7070
pub fn to_bytes(&self) -> Result<Vec<u8>, io::Error> {
7171
use std::io::Write;
7272

73-
// FIXME: Consider writing a leading version byte here so we can change AssetState's
74-
// on-disk format without silently mis-parsing old DB entries during upgrades (and fix
75-
// from_bytes accordingly).
7673
let mut bytes = Vec::new();
7774
bytes.write_all(&self.0.amount.to_bytes())?;
7875
bytes.write_u8(self.0.is_finalized as u8)?;
@@ -133,6 +130,9 @@ pub enum AssetStateError {
133130

134131
#[error("burn validation failed: {0}")]
135132
Burn(BurnError),
133+
134+
#[error("invalid input: {0}")]
135+
InvalidInput(String),
136136
}
137137

138138
/// A map of asset state changes for assets modified in a block or transaction set.
@@ -174,16 +174,14 @@ impl IssuedAssetChanges {
174174
transaction_sighashes: Option<&[SigHash]>,
175175
get_state: impl Fn(&AssetBase) -> Option<AssetState>,
176176
) -> Result<Self, AssetStateError> {
177-
// When sighashes are provided, transactions and sighashes must be equal length by design,
178-
// so we use assert instead of returning error.
179177
if let Some(sighashes) = transaction_sighashes {
180-
assert_eq!(
181-
transactions.len(),
182-
sighashes.len(),
183-
"Bug in caller: {} transactions but {} sighashes. Caller must provide one sighash per transaction.",
184-
transactions.len(),
185-
sighashes.len()
186-
);
178+
if transactions.len() != sighashes.len() {
179+
return Err(AssetStateError::InvalidInput(format!(
180+
"transaction count ({}) does not match sighash count ({})",
181+
transactions.len(),
182+
sighashes.len(),
183+
)));
184+
}
187185
}
188186

189187
// Track old and current states - old_state is None for newly created assets
@@ -206,18 +204,17 @@ impl IssuedAssetChanges {
206204
// ZIP-0227 defines issued-note rho as DeriveIssuedRho(nf_{0,0}, i_action, i_note),
207205
// so we must pass the first Action nullifier (nf_{0,0}). We rely on
208206
// `orchard_nullifiers()` preserving Action order, so `.next()` returns nf_{0,0}.
209-
let first_nullifier =
210-
// FIXME: For now, the only way to convert Zebra's nullifier type to Orchard's nullifier type
211-
// is via bytes, although they both wrap pallas::Point. Consider a more direct conversion to
212-
// avoid this round-trip, if possible.
213-
&Nullifier::from_bytes(&<[u8; 32]>::from(
214-
*tx.orchard_nullifiers()
215-
.next()
216-
// ZIP-0227 requires an issuance bundle to contain at least one OrchardZSA Action Group.
217-
// `ShieldedData.actions` is `AtLeastOne<...>`, so nf_{0,0} must exist.
218-
.expect("issuance must have at least one nullifier"),
219-
))
220-
.expect("Bytes can be converted to Nullifier");
207+
// Nullifier type conversion via bytes: both types wrap pallas::Point
208+
// but lack a direct conversion path in the current orchard API.
209+
// TODO: Consider adding a test for the case where a V6 transaction has issuance data
210+
// but has no nullifiers (the test may require constructing a proper mock V6 transaction).
211+
let raw_nullifier = tx.orchard_nullifiers().next().ok_or_else(|| {
212+
AssetStateError::InvalidInput(
213+
"issuance bundle has no orchard actions".to_string(),
214+
)
215+
})?;
216+
let first_nullifier = &Nullifier::from_bytes(&<[u8; 32]>::from(*raw_nullifier))
217+
.expect("valid zebra nullifier bytes convert to orchard nullifier");
221218

222219
let issue_records = match transaction_sighashes {
223220
Some(sighashes) => {

zebra-chain/src/orchard_zsa/issuance.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,8 @@ impl IssueData {
3838
pub(crate) fn note_commitments(&self) -> impl Iterator<Item = pallas::Base> + '_ {
3939
self.0.actions().iter().flat_map(|action| {
4040
action.notes().iter().map(|note| {
41-
// TODO: FIXME: Make `ExtractedNoteCommitment::inner` public in `orchard` (this would
42-
// eliminate the need for the workaround of converting `pallas::Base` from bytes
43-
// here), or introduce a new public method in `orchard::issuance::IssueBundle` to
44-
// retrieve note commitments directly from `orchard`.
41+
// TODO: Replace this workaround with orchard `ExtractedNoteCommitment` if its inner
42+
// field is made public, or if a note_commitments() method is added to IssueBundle.
4543
pallas::Base::from_repr(ExtractedNoteCommitment::from(note.commitment()).to_bytes())
4644
.unwrap()
4745
})

zebra-chain/src/transaction/serialize.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,14 @@ impl ZcashDeserialize for Option<orchard::ShieldedData<OrchardVanilla>> {
475475
// in [`Flags::zcash_deserialized`].
476476
let flags: orchard::Flags = (&mut reader).zcash_deserialize_into()?;
477477

478+
// `ENABLE_ZSA` is introduced in V6 (ZIP 230) and must be zero in V5.
479+
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
480+
if flags.contains(orchard::Flags::ENABLE_ZSA) {
481+
return Err(SerializationError::Parse(
482+
"ENABLE_ZSA is not allowed in V5 transactions",
483+
));
484+
}
485+
478486
// Denoted as `valueBalanceOrchard` in the spec.
479487
let value_balance: Amount = (&mut reader).zcash_deserialize_into()?;
480488

zebra-consensus/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ pub enum TransactionError {
4747
#[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")]
4848
CoinbaseHasEnableSpendsOrchard,
4949

50+
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
51+
#[error("coinbase transaction MUST NOT have the EnableZSA flag set")]
52+
CoinbaseHasEnableZSA,
53+
5054
#[error("coinbase transaction Sapling or Orchard outputs MUST be decryptable with an all-zero outgoing viewing key")]
5155
CoinbaseOutputsNotDecryptable,
5256

zebra-consensus/src/transaction/check.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,13 @@ pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), Tr
182182
if orchard_flags.contains(Flags::ENABLE_SPENDS) {
183183
return Err(TransactionError::CoinbaseHasEnableSpendsOrchard);
184184
}
185+
// ZIP-230: coinbase must not set enableZSA.
186+
// TODO: Add V6 coinbase ENABLE_ZSA tests (fails when set, passes when unset),
187+
// like v5_coinbase_transaction_without_enable_spends_flag_passes_validation.
188+
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
189+
if orchard_flags.contains(Flags::ENABLE_ZSA) {
190+
return Err(TransactionError::CoinbaseHasEnableZSA);
191+
}
185192
}
186193
}
187194

zebra-rpc/src/methods.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2096,13 +2096,25 @@ where
20962096
let read_state = self.read_state.clone();
20972097
let include_non_finalized = include_non_finalized.unwrap_or(true);
20982098

2099-
let asset_base = zebra_chain::orchard_zsa::AssetBase::from_bytes(
2100-
&hex::decode(asset_base).map_misc_error()?[..]
2101-
.try_into()
2102-
.map_misc_error()?,
2103-
)
2104-
.into_option()
2105-
.ok_or_misc_error("invalid asset base")?;
2099+
if asset_base.len() != 64 {
2100+
return Err("expected 32 bytes (64 hex chars)")
2101+
.map_error(server::error::LegacyCode::InvalidParameter);
2102+
}
2103+
2104+
let asset_base_bytes: [u8; 32] = hex::decode(&asset_base)
2105+
.map_error_with_prefix(
2106+
server::error::LegacyCode::InvalidParameter,
2107+
"invalid hex encoding",
2108+
)?
2109+
.try_into()
2110+
.expect("length already checked above");
2111+
2112+
let asset_base = zebra_chain::orchard_zsa::AssetBase::from_bytes(&asset_base_bytes)
2113+
.into_option()
2114+
.ok_or_error(
2115+
server::error::LegacyCode::InvalidParameter,
2116+
"invalid asset base",
2117+
)?;
21062118

21072119
let request = zebra_state::ReadRequest::AssetState {
21082120
asset_base,

zebra-state/src/request.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,6 @@ impl SemanticallyVerifiedBlock {
599599
new_outputs,
600600
transaction_hashes,
601601
// Not used in checkpoint paths.
602-
// FIXME: Is this correct?
603602
transaction_sighashes: None,
604603
deferred_pool_balance_change: None,
605604
}

zebra-state/src/service/non_finalized_state/chain.rs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,11 +1014,9 @@ impl Chain {
10141014
issued_asset_changes: &IssuedAssetChanges,
10151015
) {
10161016
if position == RevertPosition::Root {
1017-
// TODO: Consider evicting issued-asset entries from the non-finalized in-memory cache.
1018-
// We may add `updated_at_height` to track last update and drop “old” entries occasionally.
1019-
// Doing that here on root reverts might be too aggressive (happens ~every 100 blocks ≈ 2 hours).
1020-
// Eviction would only move reads to disk (DB); issuance/burn verification must still work,
1021-
// just with slightly worse performance due to extra DB reads.
1017+
// `issued_assets` grows monotonically (no eviction on finalization).
1018+
// At ~112 bytes per asset this is negligible for realistic asset counts.
1019+
// Revisit if ZSA adoption reaches hundreds of thousands of unique assets.
10221020
} else {
10231021
trace!(
10241022
?position,
@@ -1570,16 +1568,11 @@ impl Chain {
15701568
*current_state = *new_state;
15711569
})
15721570
.or_insert_with(|| {
1573-
// FIXME: This assert fails if the asset was removed in revert_issued_assets
1574-
// at RevertPosition::Root position. And it looks like it's a legal case
1575-
// when the previous state in block is not None but the state item was
1576-
// removed during eviction in revert_issued_assets. So we should not use
1577-
// this check here?
1578-
//assert!(
1579-
// old_state_from_block.is_none(),
1580-
// "issued asset state mismatch for {:?}",
1581-
// asset_base
1582-
//);
1571+
// When `old_state_from_block` is `Some` but the asset is missing from
1572+
// the in-memory map, it means the entry was evicted during finalization
1573+
// and lives in the finalized DB. Inserting `new_state` is correct, the
1574+
// old state is preserved on disk for rollback via the `IssuedAssetChanges`
1575+
// pairs.
15831576
*new_state
15841577
});
15851578
}

0 commit comments

Comments
 (0)