Summary
The genesis ProtocolConfig schema accepts consensusParams values as arbitrary non-negative bigint values, but these values are later packed into uint16-sized lanes.
Because the schema does not enforce the uint16 range, values greater than 65535 can be accepted during genesis config parsing and then overflow into neighboring packed fields.
This can silently produce a different on-chain consensus configuration than the one provided in the genesis config.
Affected File
scripts/genesis/ProtocolConfig.ts
Observed Behavior
The consensusParams fields are validated with schemaBigInt, but not capped to the uint16 range.
The affected fields are:
timeoutProposeMs
timeoutProposeDeltaMs
timeoutPrevoteMs
timeoutPrevoteDeltaMs
timeoutPrecommitMs
timeoutPrecommitDeltaMs
timeoutCommitMs
targetBlockTimeMs
These fields are packed into a single storage slot as 8 x uint16 values.
A value like:
timeoutProposeMs: 65536n
is accepted by the schema, but when packed into uint16-sized storage, it becomes:
timeoutProposeMs: 0
timeoutProposeDeltaMs: 1
So the value does not simply fail or clamp; it spills into the neighboring lane.
Expected Behavior
Genesis validation should reject consensusParams values outside the uint16 range before packing.
Each packed consensusParams field should be constrained to:
0 <= value <= 65535
For example:
schemaBigInt.min(0n).max(65535n)
Reproduction
I verified this with a focused repro script against the current code.
The repro uses:
timeoutProposeMs: 65536n
The schema accepts the config, then the packed result shows that the value overflows into the next 16-bit lane.
Observed output:
zod parse accepted: true
packed slot low32: 0x00010000
decoded timeoutProposeMs uint16: 0
decoded timeoutProposeDeltaMs uint16: 1
This demonstrates that:
- the invalid value is accepted by the genesis schema,
- the packed representation no longer matches the intended input,
- and the neighboring consensus parameter is modified by the overflow.
Why This Matters
Genesis configuration should either exactly represent the intended chain parameters or reject invalid values.
Accepting out-of-range bigint values before packing can result in a chain starting with consensus parameters different from those specified in the genesis file.
This is especially risky because the config appears valid at parse time, but the corruption happens during packing.
Suggested Fix
Apply uint16 bounds to every consensusParams field that is packed into a uint16 lane.
For example:
timeoutProposeMs: schemaBigInt.min(0n).max(65535n),
timeoutProposeDeltaMs: schemaBigInt.min(0n).max(65535n),
timeoutPrevoteMs: schemaBigInt.min(0n).max(65535n),
timeoutPrevoteDeltaMs: schemaBigInt.min(0n).max(65535n),
timeoutPrecommitMs: schemaBigInt.min(0n).max(65535n),
timeoutPrecommitDeltaMs: schemaBigInt.min(0n).max(65535n),
timeoutCommitMs: schemaBigInt.min(0n).max(65535n),
targetBlockTimeMs: schemaBigInt.min(0n).max(65535n),
Potential Regression Test
A regression test should verify that schemaProtocolConfig rejects consensusParams values above 65535.
Example test cases:
timeoutProposeMs: 65535n should be accepted
timeoutProposeMs: 65536n should be rejected
- each
consensusParams field should reject 65536n
Summary
The genesis
ProtocolConfigschema acceptsconsensusParamsvalues as arbitrary non-negative bigint values, but these values are later packed into uint16-sized lanes.Because the schema does not enforce the uint16 range, values greater than 65535 can be accepted during genesis config parsing and then overflow into neighboring packed fields.
This can silently produce a different on-chain consensus configuration than the one provided in the genesis config.
Affected File
scripts/genesis/ProtocolConfig.tsObserved Behavior
The
consensusParamsfields are validated withschemaBigInt, but not capped to the uint16 range.The affected fields are:
timeoutProposeMstimeoutProposeDeltaMstimeoutPrevoteMstimeoutPrevoteDeltaMstimeoutPrecommitMstimeoutPrecommitDeltaMstimeoutCommitMstargetBlockTimeMsThese fields are packed into a single storage slot as 8 x uint16 values.
A value like:
timeoutProposeMs: 65536n
is accepted by the schema, but when packed into uint16-sized storage, it becomes:
timeoutProposeMs: 0
timeoutProposeDeltaMs: 1
So the value does not simply fail or clamp; it spills into the neighboring lane.
Expected Behavior
Genesis validation should reject
consensusParamsvalues outside the uint16 range before packing.Each packed
consensusParamsfield should be constrained to:0 <= value <= 65535
For example:
schemaBigInt.min(0n).max(65535n)
Reproduction
I verified this with a focused repro script against the current code.
The repro uses:
timeoutProposeMs: 65536n
The schema accepts the config, then the packed result shows that the value overflows into the next 16-bit lane.
Observed output:
zod parse accepted: true
packed slot low32: 0x00010000
decoded timeoutProposeMs uint16: 0
decoded timeoutProposeDeltaMs uint16: 1
This demonstrates that:
Why This Matters
Genesis configuration should either exactly represent the intended chain parameters or reject invalid values.
Accepting out-of-range bigint values before packing can result in a chain starting with consensus parameters different from those specified in the genesis file.
This is especially risky because the config appears valid at parse time, but the corruption happens during packing.
Suggested Fix
Apply uint16 bounds to every
consensusParamsfield that is packed into a uint16 lane.For example:
Potential Regression Test
A regression test should verify that
schemaProtocolConfigrejectsconsensusParamsvalues above 65535.Example test cases:
timeoutProposeMs: 65535nshould be acceptedtimeoutProposeMs: 65536nshould be rejectedconsensusParamsfield should reject65536n