BLS Signing Internals
This page explains the cryptographic signing process Containment Chamber performs for Ethereum 2.0 validator operations.
Overview
Section titled “Overview”Ethereum 2.0 uses BLS12-381 signatures for all validator operations. BLS (Boneh–Lynn–Shacham) signatures provide:
- Aggregation: multiple signatures can be combined into one
- Small size: 96 bytes per signature
- Deterministic: same key + message = 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 includes:
- Domain type (4 bytes) — what operation this is
- Fork version (4 bytes) — which network/fork
- Genesis validators root (32 bytes) — which chain
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: BLS signature
Section titled “Step 4: BLS signature”Finally, the BLS signature is computed:
let signature = keypair.sk.sign(signing_root);// Returns 96-byte BLS signatureComplete Signing Flow
Section titled “Complete Signing Flow”Domain 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 | Deposits |
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- And more
tree_hash Version
Section titled “tree_hash Version”The codebase and lighthouse_types both depend on tree_hash 0.12.1 (resolved through the Lighthouse v8.1.3 tag pinned in Cargo.toml). 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”SigningRequestOwned::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); keypair.sk.sign(signing_root)}Attestation
Section titled “Attestation”SigningRequestOwned::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)); keypair.sk.sign(signing_root)}Block Proposal (BLOCK_V2)
Section titled “Block Proposal (BLOCK_V2)”SigningRequestOwned::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)); keypair.sk.sign(signing_root)}Validator Registration (special case)
Section titled “Validator Registration (special case)”SigningRequestOwned::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)); keypair.sk.sign(signing_root)}