Summary
zcash_local_net::validator::zebrad::Zebrad::launch mines a genesis block via generate_blocks(1) immediately after process spawn. With ActivationHeights::default() (which activates NU6.1 at height 1), the resulting block proposal is rejected by zebrad as a consensus failure:
zebra_rpc::methods: submit block failed verification
error=Ok(Block { source: Block { source: Other("missing lockbox disbursements for NU6.1 activation block") } })
block_hash=block::Hash("03e1c8b97d2da411a6edc433e98e683936d21b47a55ef2ffc24bb41223b3b766")
height=Height(1)
This is not a timing / readiness flake — every retry produces the identical block_hash, so every retry produces the identical rejection. Deterministic on block content.
Why it happens
zingo_common_components::protocol::ActivationHeights::default() (in zccs/dev/zingo_common_components/src/protocol.rs, around lines 39-53) sets every upgrade — including nu6_1 — to height 1:
impl Default for ActivationHeights {
fn default() -> Self {
Self::builder()
.set_overwinter(Some(1))
.set_sapling(Some(1))
.set_blossom(Some(1))
.set_heartwood(Some(1))
.set_canopy(Some(1))
.set_nu5(Some(1))
.set_nu6(Some(1))
.set_nu6_1(Some(1)) // <-- the trigger
.set_nu7(None)
.build()
}
}
So the genesis-mining block (height 1) is, by these defaults, the NU6.1 activation block. zebrad's consensus rules require the activation block to include lockbox-disbursement transactions. proposal_block_from_template (in zebra-rpc) does not generate these for the regtest fixture path, so verification fails.
Reproduction
TEST_BINARIES_DIR=~/bin cargo nextest run launch_zebrad
Affected tests in zcash_local_net:
launch_zebrad
launch_localnet_lightwalletd_zebrad
launch_localnet_zainod_zebrad
regtest-launcher::e2e::mines_once_then_exits_on_ctrlc
(Anything that goes through Zebrad::launch followed by mining the activation block.)
Workaround in place
Local override of NU6.1 activation height in ZebradConfig::default() (zcash_local_net/src/validator/zebrad.rs): set nu6_1 to Some(2) instead of inheriting Some(1) from ActivationHeights::default().
network_type: NetworkType::Regtest(
ActivationHeights::builder()
.set_overwinter(Some(1))
.set_sapling(Some(1))
.set_blossom(Some(1))
.set_heartwood(Some(1))
.set_canopy(Some(1))
.set_nu5(Some(1))
.set_nu6(Some(1))
.set_nu6_1(Some(2)) // <-- the workaround
.set_nu7(None)
.build(),
),
Genesis (height 1) is no longer the activation block, so the lockbox check doesn't fire there. Trade-off: any test that mines past height 1 will hit the same rejection at height 2, but now the failure surfaces only on tests that legitimately exercise NU6.1 — instead of being masked everywhere.
Proposed real fixes (any of)
-
Generate lockbox disbursements in the proposal builder. Either upstream — patch zebra-rpc::proposal_block_from_template to emit the required disbursements when the proposal is for an NU6.1 activation block — or wrap it locally in zcash_local_net with a helper that adds them. Most correct; restores ability to test NU6.1 on regtest fixtures.
-
Change zingo_common_components::protocol::ActivationHeights::default() to set_nu6_1(None) (or some sensible height past where most tests mine). One-line upstream edit but pushes the workaround upstream rather than fixing the underlying gap.
-
Provide a separate ActivationHeights::default_for_regtest_test_fixtures() in zingo_common_components that explicitly omits or defers nu6_1, and migrate zcash_local_net consumers to call that. Keeps the original default() faithful to current network state, separates fixture from production semantics.
(1) is the only option that preserves NU6.1 testability. (2) and (3) are pragmatic but don't actually fix the consensus-rule gap.
Companion issue
This rejection was discovered during the work to remove unconditional 5-second sleeps from Zebrad::launch (#240). The sleeps were unrelated; their removal merely surfaced this latent consensus-rule gap by exhausting the retry budget that the original 5 s wait happened to mask in some flow paths.
Summary
zcash_local_net::validator::zebrad::Zebrad::launchmines a genesis block viagenerate_blocks(1)immediately after process spawn. WithActivationHeights::default()(which activates NU6.1 at height 1), the resulting block proposal is rejected by zebrad as a consensus failure:This is not a timing / readiness flake — every retry produces the identical
block_hash, so every retry produces the identical rejection. Deterministic on block content.Why it happens
zingo_common_components::protocol::ActivationHeights::default()(inzccs/dev/zingo_common_components/src/protocol.rs, around lines 39-53) sets every upgrade — includingnu6_1— to height 1:So the genesis-mining block (height 1) is, by these defaults, the NU6.1 activation block. zebrad's consensus rules require the activation block to include lockbox-disbursement transactions.
proposal_block_from_template(inzebra-rpc) does not generate these for the regtest fixture path, so verification fails.Reproduction
TEST_BINARIES_DIR=~/bin cargo nextest run launch_zebradAffected tests in
zcash_local_net:launch_zebradlaunch_localnet_lightwalletd_zebradlaunch_localnet_zainod_zebradregtest-launcher::e2e::mines_once_then_exits_on_ctrlc(Anything that goes through
Zebrad::launchfollowed by mining the activation block.)Workaround in place
Local override of NU6.1 activation height in
ZebradConfig::default()(zcash_local_net/src/validator/zebrad.rs): setnu6_1toSome(2)instead of inheritingSome(1)fromActivationHeights::default().Genesis (height 1) is no longer the activation block, so the lockbox check doesn't fire there. Trade-off: any test that mines past height 1 will hit the same rejection at height 2, but now the failure surfaces only on tests that legitimately exercise NU6.1 — instead of being masked everywhere.
Proposed real fixes (any of)
Generate lockbox disbursements in the proposal builder. Either upstream — patch
zebra-rpc::proposal_block_from_templateto emit the required disbursements when the proposal is for an NU6.1 activation block — or wrap it locally inzcash_local_netwith a helper that adds them. Most correct; restores ability to test NU6.1 on regtest fixtures.Change
zingo_common_components::protocol::ActivationHeights::default()toset_nu6_1(None)(or some sensible height past where most tests mine). One-line upstream edit but pushes the workaround upstream rather than fixing the underlying gap.Provide a separate
ActivationHeights::default_for_regtest_test_fixtures()inzingo_common_componentsthat explicitly omits or defersnu6_1, and migratezcash_local_netconsumers to call that. Keeps the originaldefault()faithful to current network state, separates fixture from production semantics.(1) is the only option that preserves NU6.1 testability. (2) and (3) are pragmatic but don't actually fix the consensus-rule gap.
Companion issue
This rejection was discovered during the work to remove unconditional 5-second sleeps from
Zebrad::launch(#240). The sleeps were unrelated; their removal merely surfaced this latent consensus-rule gap by exhausting the retry budget that the original 5 s wait happened to mask in some flow paths.