Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Forking the Zcash Testnet with Zebra

A private fork reuses a public Testnet prefix, then applies different consensus rules without connecting to public peers. Zebra can configure the activation heights of implemented upgrades; adding a new consensus rule still requires a modified implementation. Changing a network name or branch identifier alone does not implement an upgrade.

Use Regtest instead when the test does not require public history. For a coordinated multi-implementation test, also follow Private Testnet testing.

Choose the shared prefix

Choose the first height at which the private rules differ, called the fork height. Keep the genesis block and all consensus parameters through the preceding block compatible with public Testnet. Retain historical activation heights, subsidy and funding schedules, and lockbox disbursements. No copied block may be at or beyond the first incompatible height.

The example replays NU7 at height 4,200,000 using public Testnet blocks through 4,199,999, before public NU7 activation. For a different cutoff, update the state, checkpoints, and activation table together. NU7 requires a height divisible by three under ZIP 259.

Record the prefix’s final height and block hash and share them with every participant. A different hash at the same height means the participants are not starting from the same chain.

Prepare public state

Use a dedicated public Testnet configuration, for example public-testnet.toml:

[network]
network = "Testnet"

[state]
cache_dir = "/srv/zebra-public"
delete_old_database = false

Start Zebra and synchronize until the chosen prefix is finalized:

zebrad -c public-testnet.toml start

Stop Zebra and other processes accessing this state before preparing the copy. Keep an untouched backup. The paths in this procedure must be writable by the Zebra user and must not contain another test’s state.

Prepare the snapshot with a compatible writer; follow Upgrading before copying. Do not rename version directories to bypass an upgrade.

Copy a bounded prefix

The expert copy-state command copies finalized blocks through a specified source height into another database. It does not read the source’s non-finalized backup. It uses the source network for both databases; only [state] options from its target configuration take effect. It does not convert public Testnet state directly into a custom network namespace or verify a new consensus implementation.

Save prefix-target.toml with a new, persistent destination:

[state]
cache_dir = "/srv/zebra-fork-prefix"
delete_old_database = false

Copy the example prefix:

zebrad -c public-testnet.toml copy-state \
  --max-source-height 4199999 \
  --target-config-path prefix-target.toml

With --target-config-path, ZEBRA_TARGET_STATE__CACHE_DIR overrides the target cache directory without changing the source’s configuration.

The height is inclusive. The command stops at the source’s finalized tip if it is lower than the requested height, so check the resulting prefix rather than assuming it reached the cutoff:

zebrad tip-height --network Testnet --cache-dir /srv/zebra-fork-prefix

For this example the result must be 4199999. The copy log also reports final_target_tip, including its hash; compare that hash across participants. The command commits copied blocks as checkpoint-verified blocks, rebuilding state under the source network. It is not a replacement for verifying the source chain.

The prefix is stored at /srv/zebra-fork-prefix/state/v29/testnet. Copy it into an empty private namespace:

mkdir -p /srv/zebra-nu7-private/state/v29
test ! -e /srv/zebra-nu7-private/state/v29/nu7private-00010007 && \
  cp -a /srv/zebra-fork-prefix/state/v29/testnet \
    /srv/zebra-nu7-private/state/v29/nu7private-00010007

This bounded copy finalizes every copied block, so it does not need a public non-finalized backup. Never add a backup containing public blocks beyond the cutoff.

Copy an already bounded snapshot

If the stopped source already ends below divergence, an offline directory copy avoids replaying every block. Copy its finalized database and its matching non_finalized_state/testnet directory, when present, into empty destinations with the private namespace:

Source below the public cache rootDestination below the private cache root
state/v29/testnetstate/v29/nu7private-00010007
non_finalized_state/testnetnon_finalized_state/nu7private-00010007

A non-finalized backup preserves recent blocks that are not yet in the finalized database. It can lag the live tip, so verify the restored tip before testing. Do not recover missing prefix blocks by reconnecting the private network to public seeds. Obtain a complete agreed prefix before isolation. Never copy a public peer cache into the private namespace or copy private state back into public Testnet.

Prepare private checkpoints

The checkpoint coverage rules require this prefix to reach at least height 1,028,499. A checkpoint at or after divergence would require the private chain to match the public block it replaces.

From the Zebra source checkout, extract only checkpoints below the example fork height:

awk '$1 < 4200000' \
  zebra-chain/src/parameters/checkpoint/test-checkpoints.txt \
  > /srv/zebra-nu7-private/checkpoints.txt

Share the exact file with all participants. Do not use checkpoints = true: a later Zebra release could bundle public checkpoints beyond the cutoff.

Configure the isolated network

Save the following as private-testnet.toml:

[network]
network = "Testnet"
listen_addr = "127.0.0.1:18233"
initial_testnet_peers = []
cache_dir = false

[network.testnet_parameters]
network_name = "Nu7Private"
network_magic = [0, 1, 0, 7]
checkpoints = "/srv/zebra-nu7-private/checkpoints.txt"
initial_nsm_value_balance = 55_768_414_957

[network.testnet_parameters.activation_heights]
BeforeOverwinter = 1
Overwinter = 207_500
Sapling = 280_000
Blossom = 584_000
Heartwood = 903_800
Canopy = 1_028_500
NU5 = 1_842_420
NU6 = 2_976_000
"NU6.1" = 3_536_500
"NU6.2" = 4_052_000
"NU6.3" = 4_134_000
NU7 = 4_200_000

[mining]
miner_address = "t27eWDgjFYJGVXmzrXeVjnb5J3uXDM9xH9v"

[mempool]
debug_enable_at_height = 0

[rpc]
listen_addr = "127.0.0.1:18232"
cookie_dir = "/srv/zebra-nu7-private/rpc"

[state]
cache_dir = "/srv/zebra-nu7-private"
delete_old_database = false

The custom namespace is nu7private-00010007, not the public testnet directory. The explicit NSM seed preserves the public history after changing network magic.

For multiple hosts, bind P2P to a private interface and replace initial_testnet_peers = [] with participant addresses and ports. Use the same magic and consensus settings everywhere, with the peer isolation rules.

The [mempool] debug_enable_at_height = 0 setting enables the mempool on the old copied tip, not mining. RPC remains authenticated and bound to loopback.

Mine and inspect the fork

Start the private node:

zebrad -c private-testnet.toml start

Confirm the restored tip and connected peers before producing blocks:

curl --silent --show-error \
  --user "$(cat /srv/zebra-nu7-private/rpc/.cookie)" \
  --header 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"getblockchaininfo","params":[]}' \
  http://127.0.0.1:18232/

curl --silent --show-error \
  --user "$(cat /srv/zebra-nu7-private/rpc/.cookie)" \
  --header 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":2,"method":"getpeerinfo","params":[]}' \
  http://127.0.0.1:18232/

The sample retains proof of work. Use a mining pool or the experimental internal miner. Meet the mining synchronization requirement before requesting templates; an old Testnet tip alone does not fail that check.

For tests excluding proof of work, set [network.testnet_parameters] disable_pow = true on every participant and use the generation RPCs. This does not test difficulty or Equihash validation.

After activation, compare the height and best-block hash across nodes. Inspect the upgrade status and consensus branch in getblockchaininfo, then test the new rules with transactions and blocks. Use the implemented upgrade’s branch identifier, not an arbitrary replacement. For NU7, the identifier is 0x77190ad9, specified in ZIP 259.