Networks and Forks
This page explains how Containment Chamber handles Ethereum network forks and different networks (mainnet, testnets).
Design Philosophy: Fork-Agnostic Signing
Section titled “Design Philosophy: Fork-Agnostic Signing”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
Ethereum Fork History
Section titled “Ethereum Fork History”| Fork | Version | Domain Impact |
|---|---|---|
| Phase 0 | 0x00000000 | Original beacon chain domains |
| Altair | 0x01000000 | Added sync committee domains |
| Bellatrix | 0x02000000 | Execution payload integration |
| Capella | 0x03000000 | BLS to execution changes |
| Deneb | 0x04000000 | Blob transactions |
| Electra | 0x05000000 | EIP-7251 consolidations |
| Fulu | TBD | Future fork |
The signer handles ALL of these identically — the fork version is just bytes in the domain computation.
Network Support
Section titled “Network Support”Built-in Networks
Section titled “Built-in Networks”The --network flag accepts exactly three values:
# Mainnetcontainment-chamber server --network mainnet --key-sources-filesystem-paths ./keys
# Sepolia testnetcontainment-chamber server --network sepolia --key-sources-filesystem-paths ./keys
# Hoodi testnetcontainment-chamber server --network hoodi --key-sources-filesystem-paths ./keysNetwork Separation and GVR Validation
Section titled “Network Separation and GVR Validation”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.
| Network | Identified by |
|---|---|
| Mainnet | Mainnet genesis validators root |
| Sepolia | Sepolia genesis validators root |
| Hoodi | Hoodi 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.
How Fork Compatibility Works
Section titled “How Fork Compatibility Works”Domain computation
Section titled “Domain computation”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:
- Domain type (4 bytes) — which operation (
DOMAIN_BEACON_ATTESTER,DOMAIN_BEACON_PROPOSER, …) - 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.
Example: same data, different contexts
Section titled “Example: same data, different contexts”// Mainnet Deneblet domain_mainnet = compute_domain( DOMAIN_BEACON_ATTESTER, [0x04, 0x00, 0x00, 0x00], // Deneb mainnet_genesis_root,);
// Sepolia Deneblet 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 differsFork-Specific Behavior
Section titled “Fork-Specific Behavior”Phase 0 operations
Section titled “Phase 0 operations”RANDAO_REVEALBLOCK(nowBLOCK_V2)ATTESTATIONAGGREGATION_SLOTAGGREGATE_AND_PROOFVOLUNTARY_EXIT
Altair additions (fork version 0x01000000+)
Section titled “Altair additions (fork version 0x01000000+)”SYNC_COMMITTEE_MESSAGESYNC_COMMITTEE_SELECTION_PROOFSYNC_COMMITTEE_CONTRIBUTION_AND_PROOF
Builder API (fork-independent)
Section titled “Builder API (fork-independent)”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 networkslet domain = compute_domain( DOMAIN_APPLICATION_BUILDER, [0u8; 4], Hash256::zero(),);Adding Support for New Forks
Section titled “Adding Support for New Forks”You don’t need to change the signer.
When a new Ethereum fork is released:
- Update your validator client (Lighthouse, Prysm, etc.)
- The validator client sends the new fork version in
fork_info - The signer uses it in domain computation
- 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
Testing Fork Compatibility
Section titled “Testing Fork Compatibility”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);}Best Practices
Section titled “Best Practices”For operators
Section titled “For operators”- Set
--networkcorrectly: usemainnet,hoodi, orsepoliato match your validator client’s network. The signer rejects signing requests from mismatched networks with HTTP 403. - Trust
fork_info: the request carries correct fork context — you don’t need to configure fork versions. - No manual fork handling: don’t try to override fork versions.
For developers
Section titled “For developers”- Don’t hardcode fork versions: always use values from
fork_info - Test with multiple fork versions: verify signatures differ
- Handle
ValidatorRegistrationspecially: it uses genesis fork version
Troubleshooting
Section titled “Troubleshooting””Signature verification failed”
Section titled “”Signature verification failed””Possible causes:
- Fork version mismatch between validator client and consensus layer
- Wrong
genesis_validators_root(testnet key on mainnet) - Domain type mismatch
Check: log the fork_info from requests and compare with expected values.
”Unknown fork version”
Section titled “”Unknown fork version””The signer doesn’t validate fork versions — it just uses them. If you see this error from the consensus layer, update your validator client.
Cross-network signing
Section titled “Cross-network signing”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.