Skip to content

ZebradConfig::default() proposal triggers "missing lockbox disbursements for NU6.1 activation block" #241

@zancas

Description

@zancas

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)

  1. 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.

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions