Proposal
The genesis file today holds four kinds of content:
- Identity anchors (
eth_genesis_hash, namespace) — hashed, immutable. Correct.
- Governed consensus params (stakes, epoch length, deposit/withdrawal caps, …) — hashed as initial values; the live values are chain state, changed by transaction via
ProtocolParams.sol (param_ids 0x00–0x09, flowing as execution requests into ConsensusState). Correct: pinned bootstrap value, governed live value.
- Liveness tuning (view timeouts, message size) — hashed, with no governance entry and no fork mechanism: the only fields in the file that are frozen forever. Wrong.
- Per-validator topology (
ip_address) — the only unhashed field (#[ssz(skip_serializing)]), an exception invisible to anyone holding the file. Wrong.
This issue proposes fixing 3 and 4:
- Liveness tuning → CLI flags (
RunFlags), out of the file and out of config_digest. Per-node, retunable on restart.
validators[].ip_address → --bootstrappers, deleting the field. Topology becomes per-boot config, like every other client.
Field by field, using example_genesis.toml:
eth_genesis_hash = "0x78ab9057…" # hashed — identity anchor, stays
namespace = "_SUMMIT" # hashed — identity anchor, stays
leader_timeout_ms = 2000 # hashed — tuning, frozen today → flag
notarization_timeout_ms = 4000 # hashed — tuning, frozen today → flag
nullify_timeout_ms = 4000 # hashed — tuning, frozen today → flag
skip_timeout_views = 32 # hashed — tuning, frozen today → flag
activity_timeout_views = 256 # hashed — consensus: bounds the
# vote-acceptance window (batcher's
# interesting()); stays, and is a
# governance candidate (see open
# questions)
max_message_size_bytes = 10485760 # hashed — gray zone, see open questions
validator_minimum_stake = 32000000000 # hashed — governed (ProtocolParams 0x00),
# stays as the initial value
validator_maximum_stake = 32000000000 # hashed — governed (0x01), stays
blocks_per_epoch = 10000 # hashed — governed (0x02), stays
allowed_timestamp_future_ms = 10000 # hashed — governed (0x03), stays
max_deposits_per_epoch = 3 # hashed — governed (0x05), stays
max_withdrawals_per_epoch = 16 # hashed — governed (0x06), stays
observers_per_validator = 5 # hashed — governed (0x07), stays
minimum_validator_count = 3 # hashed — governed (0x08), stays
# (defaulted fields not shown — treasury_address (0x04), invalid_deposit_tax
# (0x09) — hashed, governed, stay as initial values)
[[validators]]
node_public_key = "1be3cb06…" # hashed — identity, stays
consensus_public_key = "a6f61154…" # hashed — identity, stays
withdrawal_credentials = "0xf39F…" # hashed — identity, stays
ip_address = "127.0.0.1:26600" # NOT hashed (#[ssz(skip_serializing)])
# — topology, → --bootstrappers
End state: config_digest covers literally the whole file — no skip annotations, no invisible exceptions. Every byte of parsed content is identity (initial values for the governed params); everything operational arrives as boot config.
This is a breaking change (config_digest → chain_domain changes), so it lands at a network reset. That is the argument for doing it now, before launching mainnet.
Why
Genesis::config_digest() (types/src/genesis.rs) hashes the whole struct minus ip_address, and chain_domain(config_digest) (types/src/lib.rs) is the domain every consensus signature and p2p handshake lives under — checkpoint verification reconstructs it too. So everything in the file is network identity: nodes whose genesis differs in any field cannot handshake. For the governed params that's fine — the file only pins their initial values and the chain evolves them. For the rest of the file it's wrong three ways:
- The tuning fields are frozen by accident. They have no
ProtocolParams entry and no fork mechanism exists, so today their genesis values are final — and nothing forces this: in timeout-driven BFT, safety is independent of local timeout values under partial synchrony; only liveness degrades on mismatch. They're in the identity hash by colocation, not by necessity. Ironically the least consensus-critical fields in the file are the most frozen. (Contrast allowed_timestamp_future_ms, which feeds block validity, genuinely must be uniform — and is governed.)
- The one mutable field doesn't look mutable. An operator holding a genesis file cannot tell that editing
ip_address is safe while any other edit breaks interop — the skip-annotation asymmetry exists only in the Rust source.
- The deploy tooling's network manifest is moving to pin
config_digest as its commitment to the summit genesis, inheriting this coverage judgment wholesale — right layering, so worth getting the judgment right.
Why flags rather than new ProtocolParams entries. Adding the tuning fields as param_ids 0x0a+ would cure the frozen-ness but is the wrong tool: (a) ownership — a governed param is one value network-wide, but a view timeout is local by nature (a validator on slower disks or a distant region legitimately wants a different value); chain-governing it means an operator can't fix their own node without a governance act that retunes everyone, and safety never needed the uniformity. (b) Construction-time circularity — governed params work because the running consensus applies them to its own state at epoch boundaries, whereas these fields configure the p2p/consensus engine at startup, before the node has synced any chain state to read them from (max_message_size_bytes feeds the p2p config directly); state-driven engine knobs would need restart-on-change machinery for values that never needed agreement. The same circularity rules out IPs-on-chain in the strongest form: you need peer addresses to reach the chain at all.
Accept-side vs emit-side timing
All the timeout fields look alike, but they enter the engine on opposite sides of the message boundary (commonware-consensus 2026.4.0):
activity_timeout_views shapes what a node accepts from peers: every ingested vote passes through interesting(activity_timeout, last_finalized, current, pending, allow_future) (simplex/actors/batcher/actor.rs; predicate in simplex/mod.rs), which rejects votes for views outside [min_active(activity_timeout, last_finalized), current+1]. Nodes with different values accept different message sets — it must be uniform.
leader/notarization/nullify_timeout_ms shape only what a node emits itself: local deadlines deciding when the voter broadcasts its own nullify (simplex/actors/voter/). Incoming peer nullifies are never checked against them — a peer's early nullify makes the batcher shorten the local leader timeout (the LeaderNullify hint, simplex/actors/batcher/actor.rs), not reject the message. Mismatch costs view-synchronization liveness, nothing more.
Accept-side timing must be uniform → hashed (and a governance candidate, see open questions). Emit-side timing is a local preference → flag.
How Ethereum draws this line
- CL (beacon chain) — summit's structural twin: genesis validators live inside the pinned genesis state root, and signature domains derive from
(fork_version, genesis_validators_root), same design as chain_domain = f(config_digest). The CL validator record contains no address of any kind; bootnode ENRs ship as a separate mutable file next to the genesis (boot_enr.yaml) or as flags. (Ethereum can go further than summit: its gossip is open-membership and validators are anonymous at the network layer, so no validator→address mapping exists in the protocol at all. Summit's consensus mesh is closed — peers authenticate by validator key — so it needs a topology input; the point is that this input is boot config, not the hashed genesis.)
- The tier that never enters a spec: sync timeouts, peer counts, gossip queue sizes — client flags with client-chosen defaults, freely different per node. The rule of thumb: if it's in the chainspec it's consensus; if it's a knob, it's a flag.
- One place summit is already ahead: parameter-value changes that cost Ethereum a hardfork (EIP-7251's
MAX_EFFECTIVE_BALANCE raise, EIP-7514's churn cap) are a ProtocolParams transaction here.
Mechanics sketch
Genesis / GenesisValidator lose the tuning and topology fields; their validate() checks move to flag parsing.
- Moved fields become
RunFlags with today's example values as defaults; EngineConfig reads them from flags.
--bootstrappers becomes the single topology input for founders and joiners alike. The code is most of the way there already: ingress seeding prefers --bootstrappers over committee IPs when provided (run_node_inner); get_node_ip falls back to external-IP resolution; and a post-genesis joiner's key isn't in the genesis at all, so genesis IPs only ever matter for the founding cohort at t=0. Later, once a canonical long-lived network with stable bootnode infrastructure exists, summit can additionally embed well-known defaults per named network the way geth (params/bootnodes.go) and lighthouse (compiled-in boot_enr.yaml) do, with the flag extending them. For measured-image (TEE) deployments the flag stays primary regardless — baked-in addresses would couple the image measurement to one network's topology.
deposit_signature_domain(genesis_hash, namespace) is unaffected.
- Update
example_genesis.toml, testnet configs, genesis tooling.
Open questions
- Gray-zone fields — the test is does it feed validity (or message acceptance / participant derivation), or only liveness? Validity/acceptance stays hashed and uniform; liveness becomes a flag. Calls made so far:
activity_timeout_views stays hashed — it bounds the vote-acceptance window (commonware batcher's interesting() filters every ingested vote through it), so nodes with different values accept different message sets. observers_per_validator looked gray but is governed (0x07), already dynamic, stays. Still gray: max_message_size_bytes. For the ms view timeouts (leader/notarization/nullify_timeout_ms), the acceptance path we can find is view-windowed, not clock-windowed — a peer's early nullify shortens the local timer (the batcher's LeaderNullify hint) rather than being rejected — so they drive only the local decision to nullify; if there is an acceptance path keyed on them, they're tier-1 by this same test and the tuning tier shrinks accordingly. Engine-owner call, with a code pointer.
activity_timeout_views as a governance candidate. Uniformity-required is not the same as immutable: nothing about the value is constitutive (unlike eth_genesis_hash/namespace), so its natural home is a new ProtocolParams entry (0x0a) — genesis pins the initial value, a transaction retunes it, applied at an epoch boundary. Precedent that governed-and-engine-consumed works: EpochLength (0x02) is handled dynamically via DynamicEpocher::update_length. Needs the same dynamic plumbing for the batcher's window, plus transition semantics across the activation boundary. Same treatment would fit max_message_size_bytes if it proves consensus-coupled, though it feeds p2p construction more deeply.
namespace: hand-authored string today; deriving it from a network-level identifier would make cross-network domain separation by-construction rather than by-convention. Fold in or keep separate?
Proposal
The genesis file today holds four kinds of content:
eth_genesis_hash,namespace) — hashed, immutable. Correct.ProtocolParams.sol(param_ids0x00–0x09, flowing as execution requests intoConsensusState). Correct: pinned bootstrap value, governed live value.ip_address) — the only unhashed field (#[ssz(skip_serializing)]), an exception invisible to anyone holding the file. Wrong.This issue proposes fixing 3 and 4:
RunFlags), out of the file and out ofconfig_digest. Per-node, retunable on restart.validators[].ip_address→--bootstrappers, deleting the field. Topology becomes per-boot config, like every other client.Field by field, using
example_genesis.toml:End state:
config_digestcovers literally the whole file — no skip annotations, no invisible exceptions. Every byte of parsed content is identity (initial values for the governed params); everything operational arrives as boot config.This is a breaking change (
config_digest→chain_domainchanges), so it lands at a network reset. That is the argument for doing it now, before launching mainnet.Why
Genesis::config_digest()(types/src/genesis.rs) hashes the whole struct minusip_address, andchain_domain(config_digest)(types/src/lib.rs) is the domain every consensus signature and p2p handshake lives under — checkpoint verification reconstructs it too. So everything in the file is network identity: nodes whose genesis differs in any field cannot handshake. For the governed params that's fine — the file only pins their initial values and the chain evolves them. For the rest of the file it's wrong three ways:ProtocolParamsentry and no fork mechanism exists, so today their genesis values are final — and nothing forces this: in timeout-driven BFT, safety is independent of local timeout values under partial synchrony; only liveness degrades on mismatch. They're in the identity hash by colocation, not by necessity. Ironically the least consensus-critical fields in the file are the most frozen. (Contrastallowed_timestamp_future_ms, which feeds block validity, genuinely must be uniform — and is governed.)ip_addressis safe while any other edit breaks interop — the skip-annotation asymmetry exists only in the Rust source.config_digestas its commitment to the summit genesis, inheriting this coverage judgment wholesale — right layering, so worth getting the judgment right.Why flags rather than new
ProtocolParamsentries. Adding the tuning fields as param_ids0x0a+ would cure the frozen-ness but is the wrong tool: (a) ownership — a governed param is one value network-wide, but a view timeout is local by nature (a validator on slower disks or a distant region legitimately wants a different value); chain-governing it means an operator can't fix their own node without a governance act that retunes everyone, and safety never needed the uniformity. (b) Construction-time circularity — governed params work because the running consensus applies them to its own state at epoch boundaries, whereas these fields configure the p2p/consensus engine at startup, before the node has synced any chain state to read them from (max_message_size_bytesfeeds the p2p config directly); state-driven engine knobs would need restart-on-change machinery for values that never needed agreement. The same circularity rules out IPs-on-chain in the strongest form: you need peer addresses to reach the chain at all.Accept-side vs emit-side timing
All the timeout fields look alike, but they enter the engine on opposite sides of the message boundary (
commonware-consensus 2026.4.0):activity_timeout_viewsshapes what a node accepts from peers: every ingested vote passes throughinteresting(activity_timeout, last_finalized, current, pending, allow_future)(simplex/actors/batcher/actor.rs; predicate insimplex/mod.rs), which rejects votes for views outside[min_active(activity_timeout, last_finalized), current+1]. Nodes with different values accept different message sets — it must be uniform.leader/notarization/nullify_timeout_msshape only what a node emits itself: local deadlines deciding when the voter broadcasts its own nullify (simplex/actors/voter/). Incoming peer nullifies are never checked against them — a peer's early nullify makes the batcher shorten the local leader timeout (theLeaderNullifyhint,simplex/actors/batcher/actor.rs), not reject the message. Mismatch costs view-synchronization liveness, nothing more.Accept-side timing must be uniform → hashed (and a governance candidate, see open questions). Emit-side timing is a local preference → flag.
How Ethereum draws this line
(fork_version, genesis_validators_root), same design aschain_domain = f(config_digest). The CL validator record contains no address of any kind; bootnode ENRs ship as a separate mutable file next to the genesis (boot_enr.yaml) or as flags. (Ethereum can go further than summit: its gossip is open-membership and validators are anonymous at the network layer, so no validator→address mapping exists in the protocol at all. Summit's consensus mesh is closed — peers authenticate by validator key — so it needs a topology input; the point is that this input is boot config, not the hashed genesis.)MAX_EFFECTIVE_BALANCEraise, EIP-7514's churn cap) are aProtocolParamstransaction here.Mechanics sketch
Genesis/GenesisValidatorlose the tuning and topology fields; theirvalidate()checks move to flag parsing.RunFlagswith today's example values as defaults;EngineConfigreads them from flags.--bootstrappersbecomes the single topology input for founders and joiners alike. The code is most of the way there already: ingress seeding prefers--bootstrappersover committee IPs when provided (run_node_inner);get_node_ipfalls back to external-IP resolution; and a post-genesis joiner's key isn't in the genesis at all, so genesis IPs only ever matter for the founding cohort at t=0. Later, once a canonical long-lived network with stable bootnode infrastructure exists, summit can additionally embed well-known defaults per named network the way geth (params/bootnodes.go) and lighthouse (compiled-inboot_enr.yaml) do, with the flag extending them. For measured-image (TEE) deployments the flag stays primary regardless — baked-in addresses would couple the image measurement to one network's topology.deposit_signature_domain(genesis_hash, namespace)is unaffected.example_genesis.toml, testnet configs, genesis tooling.Open questions
activity_timeout_viewsstays hashed — it bounds the vote-acceptance window (commonware batcher'sinteresting()filters every ingested vote through it), so nodes with different values accept different message sets.observers_per_validatorlooked gray but is governed (0x07), already dynamic, stays. Still gray:max_message_size_bytes. For the ms view timeouts (leader/notarization/nullify_timeout_ms), the acceptance path we can find is view-windowed, not clock-windowed — a peer's early nullify shortens the local timer (the batcher'sLeaderNullifyhint) rather than being rejected — so they drive only the local decision to nullify; if there is an acceptance path keyed on them, they're tier-1 by this same test and the tuning tier shrinks accordingly. Engine-owner call, with a code pointer.activity_timeout_viewsas a governance candidate. Uniformity-required is not the same as immutable: nothing about the value is constitutive (unlikeeth_genesis_hash/namespace), so its natural home is a newProtocolParamsentry (0x0a) — genesis pins the initial value, a transaction retunes it, applied at an epoch boundary. Precedent that governed-and-engine-consumed works:EpochLength(0x02) is handled dynamically viaDynamicEpocher::update_length. Needs the same dynamic plumbing for the batcher's window, plus transition semantics across the activation boundary. Same treatment would fitmax_message_size_bytesif it proves consensus-coupled, though it feeds p2p construction more deeply.namespace: hand-authored string today; deriving it from a network-level identifier would make cross-network domain separation by-construction rather than by-convention. Fold in or keep separate?