Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6ed5934
feat(consensus): implement ZIP 218 behind a build feature
evan-forbes Sep 1, 2026
5655d94
docs(changelog): add the ZIP 218 fragment
evan-forbes Sep 1, 2026
7287d80
fix(state): derive the seeded validation context from the span constant
evan-forbes Sep 1, 2026
18b2aeb
test(zakurad): store the v1.4.0 config for compatibility testing
evan-forbes Sep 1, 2026
ab9c022
fix(rpc): reserve ZIP 218 coinbase actions
evan-forbes Sep 5, 2026
18f5ca9
fix(header-chain): restore the conformance test name
evan-forbes Sep 5, 2026
8f0bb31
fix(consensus): handle ZIP 218 upgrade boundaries
evan-forbes Sep 7, 2026
2f9a060
build: publish ZIP 218 features after v1.4.0-rc0
evan-forbes Sep 7, 2026
2f7a909
fix(chain): scope the ZIP 218 regression import
evan-forbes Sep 7, 2026
123c7cf
refactor(build): mark NU7 feature experimental
evan-forbes Sep 7, 2026
b504183
build: publish the chain dependency cascade
evan-forbes Sep 7, 2026
b648a19
Merge remote-tracking branch 'origin/main' into feat/zip218-blocktime…
evan-forbes Sep 9, 2026
c19dad1
feat(consensus): disallow version 4 transactions at NU7 (#856)
evan-forbes Sep 9, 2026
6271f21
Merge remote-tracking branch 'origin/main' into feat/zip218-blocktime…
evan-forbes Sep 10, 2026
3c9b211
Merge remote-tracking branch 'origin/main' into feat/zip218-blocktime…
evan-forbes Sep 11, 2026
703c289
docs(changelog): fold the ZIP 2003 entry into the ZIP 218 fragment
evan-forbes Sep 11, 2026
cc8bb52
build: bump the crates that gain NU7 features after v1.4.0
evan-forbes Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/tests-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ jobs:
id-token: write
statuses: write
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
Expand All @@ -358,8 +358,10 @@ jobs:
- uses: ./.github/actions/setup-zakura-build
- name: Test experimental NU7 builds
run: |
cargo test --locked --features zakura-chain/nu7-experimental \
-p zakura-chain -p zakura-header-chain --lib
cargo test --locked --features nu7-experimental \
-p zakura-consensus --lib
-p zakura-consensus -p zakura-rpc --lib
cargo check --locked -p zakura --features nu7-experimental

network-dependent:
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion crates/zakura-chain/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "zakura-chain"
version = "7.0.0"
version = "7.0.1"
authors.workspace = true
description = "Core Zcash data structures for the Zakura node. Internal crate, published to support cargo install zakura"
license.workspace = true
Expand All @@ -20,6 +20,15 @@ default = []

# Production features that activate extra functionality

# Experimental candidate NU7 consensus rules, currently including ZIP 218's
# shorter target spacing, wider difficulty averaging window, reduced subsidy by
# the spacing ratio, and per-block shielded action limits.
#
# The rules stay dormant until NU7 has an activation height on the configured
# network, so this feature only changes consensus on a network that configures
# one.
nu7-experimental = []

# Consensus-critical conversion from JSON to Zcash types
json-conversion = [
"serde_json",
Expand Down
21 changes: 21 additions & 0 deletions crates/zakura-chain/src/parameters/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,27 @@ impl Network {
.collect()
}

/// Returns the first height at which version 4 transactions are invalid.
///
/// [ZIP 2003] deprecates version 4 transactions at NU7. The
/// `nu7-experimental` feature must be enabled. A network that does not
/// activate NU7 keeps accepting them.
///
/// [ZIP 2003]: https://zips.z.cash/zip-2003
pub fn v4_deprecation_height(&self) -> Option<Height> {
if cfg!(feature = "nu7-experimental") {
NetworkUpgrade::Nu7.activation_height(self)
} else {
None
}
}

/// Returns whether version 4 transactions are invalid at `height` on this network.
pub fn is_v4_deprecated(&self, height: Height) -> bool {
self.v4_deprecation_height()
.is_some_and(|deprecation_height| height >= deprecation_height)
}

/// Returns the height at which the soft fork that temporarily disables Orchard
/// actions in transactions activates, if it is configured for this network.
pub fn temporary_orchard_disabling_soft_fork_height(&self) -> Option<Height> {
Expand Down
115 changes: 61 additions & 54 deletions crates/zakura-chain/src/parameters/network/subsidy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,24 +279,19 @@ impl ParameterSubsidy for Network {
/// as described in [protocol specification §7.10][7.10]
///
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
pub fn funding_stream_address_period<N: ParameterSubsidy>(height: Height, network: &N) -> u32 {
pub fn funding_stream_address_period<N: ParameterSubsidy>(
height: Height,
network: &N,
) -> HeightDiff {
// Spec equation: `address_period = floor((height - (height_for_halving(1) - post_blossom_halving_interval))/funding_stream_address_change_interval)`,
// <https://zips.z.cash/protocol/protocol.pdf#fundingstreams>
//
// Note that the brackets make it so the post blossom halving interval is added to the total.
//
// In Rust, "integer division rounds towards zero":
// <https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators>
// This is the same as `floor()`, because these numbers are all positive.

let height_after_first_halving = height - network.height_for_first_halving();

let address_period = (height_after_first_halving + network.post_blossom_halving_interval())
/ network.funding_stream_address_change_interval();

address_period
.try_into()
.expect("all values are positive and smaller than the input height")
(height_after_first_halving + network.post_blossom_halving_interval())
.div_euclid(network.funding_stream_address_change_interval())
}

/// The first block height of the halving at the provided halving index for a network.
Expand All @@ -309,26 +304,24 @@ pub fn height_for_halving(halving: u32, network: &Network) -> Option<Height> {
return Some(Height(0));
}

let slow_start_shift = i64::from(network.slow_start_shift().0);
let blossom_height = i64::from(NetworkUpgrade::Blossom.activation_height(network)?.0);
let pre_blossom_halving_interval = network.pre_blossom_halving_interval();
let halving_index = i64::from(halving);

let unscaled_height = halving_index.checked_mul(pre_blossom_halving_interval)?;

let pre_blossom_height = unscaled_height
.min(blossom_height)
.checked_add(slow_start_shift)?;

let post_blossom_height = 0
.max(unscaled_height - blossom_height)
.checked_mul(i64::from(BLOSSOM_POW_TARGET_SPACING_RATIO))?
.checked_add(slow_start_shift)?;
if self::halving(Height::MAX, network) < halving {
return None;
}

let height = pre_blossom_height.checked_add(post_blossom_height)?;
// `halving` is monotonic. Search its complete height domain so this inverse
// automatically includes every target-spacing era.
let mut low = Height::MIN.0;
let mut high = Height::MAX.0;
while low < high {
let middle = low + (high - low) / 2;
if self::halving(Height(middle), network) < halving {
low = middle + 1;
} else {
high = middle;
}
}

let height = u32::try_from(height).ok()?;
height.try_into().ok()
Some(Height(low))
}

/// Returns the `fs.Value(height)` for each stream receiver
Expand Down Expand Up @@ -417,28 +410,38 @@ pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
pub fn halving(height: Height, network: &Network) -> u32 {
let slow_start_shift = network.slow_start_shift();
let blossom_height = NetworkUpgrade::Blossom
.activation_height(network)
.expect("blossom activation height should be available");

let halving_index = if height < slow_start_shift {
0
} else if height < blossom_height {
let pre_blossom_height = height - slow_start_shift;
pre_blossom_height / network.pre_blossom_halving_interval()
} else {
let pre_blossom_height = blossom_height - slow_start_shift;
let scaled_pre_blossom_height =
pre_blossom_height * HeightDiff::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
if height < slow_start_shift {
return 0;
}

let post_blossom_height = height - blossom_height;
// Each target spacing era contributes (blocks in the era * era spacing) to a
// running total of block seconds, which the pre-Blossom halving interval
// measured in seconds then divides. This is the spec's segmented sum of
// fractions with the common denominator factored out, so it stays in integer
// arithmetic no matter how many spacing eras a network has. ZIP 218 adds a
// third era at NU7.
let pre_blossom_spacing_seconds = NetworkUpgrade::Genesis.target_spacing().num_seconds();
let mut total_block_seconds: HeightDiff = 0;

let mut eras = NetworkUpgrade::target_spacings(network)
Comment thread
evan-forbes marked this conversation as resolved.
.filter(|(era_start, _)| *era_start <= height)
.peekable();

while let Some((era_start, era_spacing)) = eras.next() {
let era_end = eras
.peek()
.map(|(next_start, _)| *next_start)
.unwrap_or(height);
let era_blocks = (era_end - era_start.max(slow_start_shift)).max(0);
total_block_seconds += era_blocks * era_spacing.num_seconds();
}

(scaled_pre_blossom_height + post_blossom_height) / network.post_blossom_halving_interval()
};
let pre_blossom_denominator =
network.pre_blossom_halving_interval() * pre_blossom_spacing_seconds;

halving_index
(total_block_seconds / pre_blossom_denominator)
.try_into()
.expect("already checked for negatives")
.expect("halving index is non-negative and fits in u32")
}

/// `BlockSubsidy(height)` as described in [protocol specification §7.8][7.8]
Expand All @@ -462,13 +465,17 @@ pub fn block_subsidy(height: Height, net: &Network) -> Result<Amount<NonNegative
slow_start_rate * (u64::from(height) + 1)
}
} else {
let base_subsidy = if NetworkUpgrade::current(net, height) < NetworkUpgrade::Blossom {
MAX_BLOCK_SUBSIDY
} else {
MAX_BLOCK_SUBSIDY / u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO)
};

base_subsidy / halving_div
// Each spacing era scales the per-block subsidy by
// `current_spacing / pre_blossom_spacing`, which keeps issuance per unit of
// wall-clock time constant across spacing changes. Blossom divides the
// subsidy by 2, and ZIP 218 divides it by a further 3 at NU7. The casts are
// safe because target spacings are small positive constants.
let current_spacing_seconds =
NetworkUpgrade::target_spacing_for_height(net, height).num_seconds() as u64;
let pre_blossom_spacing_seconds =
NetworkUpgrade::Genesis.target_spacing().num_seconds() as u64;

MAX_BLOCK_SUBSIDY * current_spacing_seconds / pre_blossom_spacing_seconds / halving_div
};

Ok(Amount::try_from(amount)?)
Expand Down
6 changes: 4 additions & 2 deletions crates/zakura-chain/src/parameters/network/testnet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ fn num_funding_stream_addresses_required_for_height_range(
height_range: &std::ops::Range<Height>,
network: &Network,
) -> usize {
1u32.checked_add(funding_stream_address_period(
1i64.checked_add(funding_stream_address_period(
height_range
.end
.previous()
Expand All @@ -324,7 +324,9 @@ fn num_funding_stream_addresses_required_for_height_range(
))
.expect("no overflow should happen in this sum")
.checked_sub(funding_stream_address_period(height_range.start, network))
.expect("no overflow should happen in this sub") as usize
.expect("no overflow should happen in this sub")
.try_into()
.expect("a funding stream height range must not have a negative number of periods")
}

/// Checks that the provided [`FundingStreams`] has sufficient recipient addresses for the
Expand Down
Loading
Loading