Domain separation, fork data root, signing root computation, and per-operation signing logic in Containment Chamber
This page explains the cryptographic signing process Containment Chamber performs for Ethereum consensus-layer validator operations.
Overview
Section titled “Overview”Ethereum consensus-layer validator operations use BLS12-381 signatures. BLS (Boneh–Lynn–Shacham) signatures provide:
- Aggregation: multiple signatures can be combined into one
- Small size: 96 bytes per signature
- Deterministic: same key + signing root = same signature
The Signing Process
Section titled “The Signing Process”Step 1: Domain separation
Section titled “Step 1: Domain separation”Every signature includes a domain to prevent replay attacks across different contexts:
// Domain type constants (first 4 bytes)pub const DOMAIN_BEACON_PROPOSER: [u8; 4] = [0x00, 0x00, 0x00, 0x00];pub const DOMAIN_BEACON_ATTESTER: [u8; 4] = [0x01, 0x00, 0x00, 0x00];pub const DOMAIN_RANDAO: [u8; 4] = [0x02, 0x00, 0x00, 0x00];pub const DOMAIN_VOLUNTARY_EXIT: [u8; 4] = [0x04, 0x00, 0x00, 0x00];// ... and moreDomain computation inputs are:
- Domain type (4 bytes) — what operation this is
- Fork version (4 bytes) — the fork context supplied by
fork_info.fork.current_version - Genesis validators root (32 bytes) — the chain identity supplied by
fork_info.genesis_validators_root
pub fn compute_domain( domain_type: [u8; 4], fork_version: [u8; 4], genesis_validators_root: Hash256,) -> [u8; 32] { let fork_data_root = compute_fork_data_root(fork_version, genesis_validators_root);
let mut domain = [0u8; 32]; domain[..4].copy_from_slice(&domain_type); domain[4..].copy_from_slice(&fork_data_root.as_slice()[..28]); domain}Why domains matter:
- An attestation signature cannot be replayed as a block proposal
- A mainnet signature cannot be replayed on testnet
- A Phase 0 signature cannot be replayed after a fork
Step 2: Fork data root
Section titled “Step 2: Fork data root”The fork data root combines fork version with chain identity:
#[derive(TreeHash)]struct ForkData { current_version: [u8; 4], genesis_validators_root: Hash256,}
pub fn compute_fork_data_root( fork_version: [u8; 4], genesis_validators_root: Hash256,) -> Hash256 { ForkData { current_version: fork_version, genesis_validators_root, }.tree_hash_root()}Step 3: Signing root
Section titled “Step 3: Signing root”The signing root combines the object being signed with the domain:
#[derive(TreeHash)]pub struct SigningData { pub object_root: Hash256, pub domain: [u8; 32],}
pub fn compute_signing_root<T: TreeHash>(object: &T, domain: [u8; 32]) -> Hash256 { SigningData { object_root: object.tree_hash_root(), domain, }.tree_hash_root()}The signing root is the actual 32-byte value that gets signed by BLS.
Step 4: Safety checks and BLS signature
Section titled “Step 4: Safety checks and BLS signature”Before signing, the HTTP path:
- Verifies authorization for the validator public key and signing operation
- Checks the signer is unsealed and the state watcher is fresh for stateful deployments
- Rejects mismatched
genesis_validators_rootvalues before anti-slashing - Looks up the requested validator key
- Records the operation in the anti-slashing backend
Only after those checks pass is the BLS signature computed:
let signature = keypair.sk.sign(signing_root);// Returns 96-byte BLS signatureComplete Signing Flow
Section titled “Complete Signing Flow”direction: down
req: "Signing Request\ntype: ATTESTATION\ndata: {...}\nfork_info: {...}"extract: "1. Extract Fork Info\nfork_version = fork_info.fork.current_version\ngenesis_root = fork_info.genesis_validators_root"domain: "2. Compute Domain\ncompute_domain(\n DOMAIN_BEACON_ATTESTER (0x01000000),\n fork_version,\n genesis_root\n)"root: "3. Compute Signing Root\nSignedRoot::signing_root(domain)\nor compute_signing_root(&value, domain)"safety: "4. Safety Checks\nnetwork GVR guard\nauth + key lookup\nanti-slashing check"sign: "5. BLS Sign\nkeypair.sk.sign(signing_root)\n96-byte BLS12-381 signature"resp: "6. Return Response\n{ signature: 0x... }\nhex-encoded 96 bytes"
req.style.fill: "#FFF6EF"req.style.stroke: "#D35F0A"req.style.font-color: "#170206"extract.style.fill: "#FEEC8C"extract.style.stroke: "#D35F0A"extract.style.font-color: "#170206"domain.style.fill: "#FEEC8C"domain.style.stroke: "#D35F0A"domain.style.font-color: "#170206"root.style.fill: "#FEEC8C"root.style.stroke: "#D35F0A"root.style.font-color: "#170206"safety.style.fill: "#FEE2E2"safety.style.stroke: "#DC2626"safety.style.font-color: "#170206"sign.style.fill: "#CAF2E6"sign.style.stroke: "#13A477"sign.style.font-color: "#170206"resp.style.fill: "#CAF2E6"resp.style.stroke: "#13A477"resp.style.font-color: "#170206"
req -> extractextract -> domaindomain -> rootroot -> safetysafety -> signsign -> respDomain Types Reference
Section titled “Domain Types Reference”| Constant | Value | Operation |
|---|---|---|
DOMAIN_BEACON_PROPOSER |
0x00000000 |
Block proposals |
DOMAIN_BEACON_ATTESTER |
0x01000000 |
Attestations |
DOMAIN_RANDAO |
0x02000000 |
RANDAO reveals |
DOMAIN_DEPOSIT |
0x03000000 |
Deposit data and credentials transfer, not an HTTP signing request |
DOMAIN_VOLUNTARY_EXIT |
0x04000000 |
Voluntary exits |
DOMAIN_SELECTION_PROOF |
0x05000000 |
Aggregator selection |
DOMAIN_AGGREGATE_AND_PROOF |
0x06000000 |
Aggregate attestations |
DOMAIN_SYNC_COMMITTEE |
0x07000000 |
Sync committee messages |
DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF |
0x08000000 |
Sync committee selection |
DOMAIN_CONTRIBUTION_AND_PROOF |
0x09000000 |
Sync committee contributions |
DOMAIN_APPLICATION_BUILDER |
0x00000001 |
Builder API (MEV) |
SignedRoot Trait
Section titled “SignedRoot Trait”Lighthouse types implement the SignedRoot trait, which provides a signing_root() method:
pub trait SignedRoot: TreeHash { fn signing_root(&self, domain: Hash256) -> Hash256 { SigningData { object_root: self.tree_hash_root(), domain, } .tree_hash_root() }}Types implementing SignedRoot:
AttestationDataBeaconBlockHeaderVoluntaryExitSyncAggregatorSelectionDataValidatorRegistrationDataAggregateAndProofContributionAndProof
Containment Chamber uses SignedRoot for attestation data, block headers, exits, sync aggregator selection data, validator registrations, aggregates, and sync contributions. It uses compute_signing_root() directly for scalar values such as RANDAO epochs, aggregation slots, and sync committee block roots.
tree_hash Version
Section titled “tree_hash Version”The codebase and lighthouse_types both depend on the same tree_hash 0.12.x line (resolved through the Lighthouse v8.1.3 tag pinned in Cargo.toml; see Cargo.lock for the exact patch version). No version-bridging conversion is required — SignedRoot::signing_root() from lighthouse_types and the codebase’s own compute_signing_root interoperate directly.
Operation-Specific Signing
Section titled “Operation-Specific Signing”RANDAO Reveal
Section titled “RANDAO Reveal”SigningRequest::RandaoReveal { randao_reveal, fork_info } => { let domain = compute_domain(DOMAIN_RANDAO, fork_version, genesis_root); let signing_root = compute_signing_root(&randao_reveal.epoch.as_u64(), domain); anti_slashing.check_and_update(pubkey, generic("RANDAO")).await?; keypair.sk.sign(signing_root)}Attestation
Section titled “Attestation”SigningRequest::Attestation { attestation, fork_info } => { let domain = compute_domain(DOMAIN_BEACON_ATTESTER, fork_version, genesis_root); let signing_root = attestation.data.signing_root(Hash256::from(domain)); anti_slashing.check_and_update(pubkey, attestation_watermark).await?; keypair.sk.sign(signing_root)}Block Proposal (BLOCK_V2)
Section titled “Block Proposal (BLOCK_V2)”SigningRequest::BlockV2 { beacon_block, fork_info, .. } => { let domain = compute_domain(DOMAIN_BEACON_PROPOSER, fork_version, genesis_root); let signing_root = beacon_block.block_header.signing_root(Hash256::from(domain)); anti_slashing.check_and_update(pubkey, block_watermark).await?; keypair.sk.sign(signing_root)}Validator Registration (special case)
Section titled “Validator Registration (special case)”SigningRequest::ValidatorRegistration { validator_registration, .. } => { // Uses DOMAIN_APPLICATION_BUILDER with genesis fork version [0,0,0,0] // and zero genesis_validators_root — fork-independent. let domain = compute_domain( DOMAIN_APPLICATION_BUILDER, [0u8; 4], // Genesis fork version Hash256::zero(), // Zero validators root ); let signing_root = validator_registration .registration .signing_root(Hash256::from(domain)); anti_slashing.check_and_update(pubkey, generic("VALIDATOR_REGISTRATION")).await?; keypair.sk.sign(signing_root)}Security Considerations
Section titled “Security Considerations”Next Steps
Section titled “Next Steps”- Anti-Slashing — how the protection database prevents slashable signatures
- Production Hardening — memory locking, core dump prevention, and audit logging
- Auth Policies & Tokens — restrict which keys and operations each client can access

