Skip to content

Networks and Forks

This page explains how Containment Chamber handles Ethereum network forks and different networks (mainnet, testnets).

Key insight: the remote signer does NOT need to know about specific forks.

The signer receives fork_info with every signing request:

{
"type": "ATTESTATION",
"fork_info": {
"fork": {
"previous_version": "0x04000000",
"current_version": "0x05000000",
"epoch": "0"
},
"genesis_validators_root": "0x4b363db94e286120d76eb905340fcd4e8a81b2147cfb21a2..."
},
"attestation": { ... }
}

This means:

  • The validator client handles fork transitions
  • The signer just uses the provided fork version
  • No code changes needed for new forks
  • Supports ALL forks: Phase 0, Altair, Bellatrix, Capella, Deneb, Electra, Fulu, and beyond
ForkVersionDomain Impact
Phase 00x00000000Original beacon chain domains
Altair0x01000000Added sync committee domains
Bellatrix0x02000000Execution payload integration
Capella0x03000000BLS to execution changes
Deneb0x04000000Blob transactions
Electra0x05000000EIP-7251 consolidations
FuluTBDFuture fork

The signer handles ALL of these identically — the fork version is just bytes in the domain computation.

The --network flag accepts exactly three values:

Terminal window
# Mainnet
containment-chamber server --network mainnet --key-sources-filesystem-paths ./keys
# Sepolia testnet
containment-chamber server --network sepolia --key-sources-filesystem-paths ./keys
# Hoodi testnet
containment-chamber server --network hoodi --key-sources-filesystem-paths ./keys

Networks are identified by genesis_validators_root (GVR). The signer validates the GVR in every signing request against the configured --network. A mismatch returns HTTP 403 Forbidden, stopping the request before any signing or anti-slashing checks occur.

NetworkIdentified by
MainnetMainnet genesis validators root
SepoliaSepolia genesis validators root
HoodiHoodi genesis validators root

This is a safety guardrail. If your validator client is accidentally pointed at the wrong signer, or if a request carries the wrong network’s GVR, the signer refuses outright rather than producing a signature that would fail on-chain.

pub fn compute_domain(
domain_type: [u8; 4], // Operation type (attestation, block, etc.)
fork_version: [u8; 4], // From fork_info.fork.current_version
genesis_validators_root: Hash256, // From fork_info.genesis_validators_root
) -> [u8; 32]

The 32-byte domain is split into two regions:

  1. Domain type (4 bytes) — which operation (DOMAIN_BEACON_ATTESTER, DOMAIN_BEACON_PROPOSER, …)
  2. Fork data root (28 bytes) — the leading 28 bytes of tree_hash_root({current_version, genesis_validators_root}). Fork version and genesis root are SSZ-hashed together; they do not occupy independent byte ranges.

This means:

  • Same attestation data on different forks → different signatures
  • Same attestation data on different networks → different signatures
  • Cryptographic guarantee against cross-fork/cross-network replay

See BLS Signing Internals for the full domain computation.

// Mainnet Deneb
let domain_mainnet = compute_domain(
DOMAIN_BEACON_ATTESTER,
[0x04, 0x00, 0x00, 0x00], // Deneb
mainnet_genesis_root,
);
// Sepolia Deneb
let domain_sepolia = compute_domain(
DOMAIN_BEACON_ATTESTER,
[0x04, 0x00, 0x00, 0x00], // Also Deneb
sepolia_genesis_root, // Different!
);
// domain_mainnet != domain_sepolia
// Therefore: signing_root differs, signature differs
  • RANDAO_REVEAL
  • BLOCK (now BLOCK_V2)
  • ATTESTATION
  • AGGREGATION_SLOT
  • AGGREGATE_AND_PROOF
  • VOLUNTARY_EXIT

Altair additions (fork version 0x01000000+)

Section titled “Altair additions (fork version 0x01000000+)”
  • SYNC_COMMITTEE_MESSAGE
  • SYNC_COMMITTEE_SELECTION_PROOF
  • SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF
  • VALIDATOR_REGISTRATION

The VALIDATOR_REGISTRATION operation is special:

// Always uses genesis fork version [0,0,0,0]
// Always uses zero genesis_validators_root
// This makes it work across ALL forks and networks
let domain = compute_domain(
DOMAIN_APPLICATION_BUILDER,
[0u8; 4],
Hash256::zero(),
);

You don’t need to change the signer.

When a new Ethereum fork is released:

  1. Update your validator client (Lighthouse, Prysm, etc.)
  2. The validator client sends the new fork version in fork_info
  3. The signer uses it in domain computation
  4. Everything works

The only time you’d need to change the signer is if:

  • A new domain type is added (new DOMAIN_* constant)
  • A completely new signing flow is introduced

The test suite verifies fork handling:

#[test]
fn test_different_domains_different_signatures() {
// Same epoch, different domain types → different signatures
let randao_sig = sign_with_domain(DOMAIN_RANDAO);
let attester_sig = sign_with_domain(DOMAIN_BEACON_ATTESTER);
assert_ne!(randao_sig, attester_sig);
}
#[test]
fn test_different_forks_different_signatures() {
// Same data, different fork versions → different signatures
let phase0_sig = sign_with_fork([0x00, 0x00, 0x00, 0x00]);
let altair_sig = sign_with_fork([0x01, 0x00, 0x00, 0x00]);
assert_ne!(phase0_sig, altair_sig);
}
  1. Set --network correctly: use mainnet, hoodi, or sepolia to match your validator client’s network. The signer rejects signing requests from mismatched networks with HTTP 403.
  2. Trust fork_info: the request carries correct fork context — you don’t need to configure fork versions.
  3. No manual fork handling: don’t try to override fork versions.
  1. Don’t hardcode fork versions: always use values from fork_info
  2. Test with multiple fork versions: verify signatures differ
  3. Handle ValidatorRegistration specially: it uses genesis fork version

Possible causes:

  1. Fork version mismatch between validator client and consensus layer
  2. Wrong genesis_validators_root (testnet key on mainnet)
  3. Domain type mismatch

Check: log the fork_info from requests and compare with expected values.

The signer doesn’t validate fork versions — it just uses them. If you see this error from the consensus layer, update your validator client.

This is prevented by design. A signature created with mainnet’s genesis_validators_root will never verify on Sepolia, and vice versa. The domain computation ensures cryptographic separation.