Repository navigation
fix(params,genesis): declare constantinople/petersburg/istanbul blocks - #2777
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
1860f14 to
6d03ab9
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The consensus configuration change intentionally rejects some existing private-chain schedules and therefore warrants final human compatibility review.
0 open findings
What changed in this PR
Declares effective Constantinople, Petersburg, and Istanbul activation heights for built-in XDC networks while preserving existing execution and fork IDs.
Changes:
- Adds explicit fork heights to network and genesis configurations.
- Reorders fork validation and generated metadata.
- Updates fixtures, semantic hashes, and regression coverage.
| File | Description |
|---|---|
params/config_test.go |
Verifies stable fork lists. |
params/config_networks.go |
Declares effective fork heights. |
params/config_networks_test.go |
Validates declarations and active forks. |
params/config_backfill_test.go |
Tests validation ordering and incompatibilities. |
params/config_backfill_generator.go |
Documents generated ordering semantics. |
params/config_backfill_generated.go |
Updates generated fork order. |
params/config_backfill_fields.json |
Moves the trio after cancellation-fee activation. |
internal/ethapi/simulate_tipsigning_test.go |
Updates simulation fixture ordering. |
genesis/testnet.json |
Adds testnet fork declarations. |
genesis/mainnet.json |
Adds mainnet fork declarations. |
genesis/devnet.json |
Adds devnet fork declarations. |
eth/tracers/api_tipsigning_activation_test.go |
Updates tracing fixture ordering. |
eth/state_accessor_tipsigning_test.go |
Updates state-access fixture ordering. |
core/chainconfig_equal_test.go |
Updates semantic hash vectors. |
core/blockchain_test.go |
Moves the trio in future-fork fixtures. |
contracts/utils_test.go |
Updates legacy contract fixture. |
consensus/tests/engine_v2_tests/helper.go |
Updates V2 legacy execution configuration. |
consensus/tests/engine_v1_tests/helper.go |
Updates V1 legacy execution configuration. |
Files not reviewed (1)
- params/config_backfill_generated.go: Generated file
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
89d1e62 to
c798c40
Compare
cd409e3 to
d3fb647
Compare
f8b3770 to
2137d34
Compare
2137d34 to
c77d036
Compare
ec81679 to
0b150fa
Compare
872963c to
17772ec
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Stored consensus-config migration needs maintainer confirmation of private-chain compatibility, and a startup repair diagnostic remains unresolved.
1 open issue
2 resolved since last review
1 file not reviewed
params/config_backfill_generated.go: Generated file
Review effort: Balanced
538be51 to
ee21e4a
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Consensus-sensitive startup compatibility needs maintainer review, and a regression-test assertion still needs correction.
1 open issue
1 resolved since last review
1 file not reviewed
params/config_backfill_generated.go: Generated file
Review effort: Balanced
The built-in XDC network configs left ConstantinopleBlock,
PetersburgBlock and IstanbulBlock unset. At runtime
ChainConfig.IsPetersburg/IsIstanbul fall back to
TIPXDCXCancellationFeeBlock, so those two forks activated at the
cancellation-fee height (mainnet 38383838, testnet 23779191,
devnet 0), while IsConstantinople has no fallback and was never
true on any of the three chains.
Declare the three slots explicitly at those heights, following the
upstream convention of setting a skipped fork at its successor's
block (geth mainnet sets Constantinople and Petersburg both at
7280000), and mirror the values into the embedded
genesis/{mainnet,testnet,devnet}.json so the two startup paths stay
in lockstep. In the genesis files the trio keys sit next to
DenylistBlock, the height they share, rather than after
byzantiumBlock: those files group fields by activation height and
their key order is an append history that has never matched the Go
struct order.
Execution is unchanged: GatherForks (hence forkid) is identical
before and after, Istanbul rules were already active at these
heights through the fallback, and the newly-true
IsConstantinople only adds an ActiveForks entry.
Because the slots now carry values, the fork-order validation
moves them after TIPXDCXCancellationFeeBlock; without that, filling
them is rejected with "IstanbulBlock 38383838 > TIPSigningBlock
3000000". The rejection also names the field to move: both the
ordered scan and the special-case rules append the fix — declare
the later field at or after the earlier one, plus, when that field
is not mandatory for the config's shape, that dropping it works
too — instead of leaving the operator to work out which of the two
fields to move. The eight slots between Byzantium and the trio
(TIPSigningBlock..TIPXDCXCancellationFeeBlock, including DenylistBlock)
shift down by three so the ordering key stays dense at 1..34 and
the trio takes 16..18. The standard slots keep their relative
order (Constantinople 16 still sits before Berlin 20), so the
sequence scan reads the same sequence it did before. Test
fixtures that push the XDC forks into the future carry the trio
together for the same reason, and new tests pin the declared heights,
the unchanged fork id, and the configs the new position refuses. The
built-in Localnet config stays unfilled, because it is the hydration
source for custom chain configs.
Nodes running a stored config from before this change need no
action: the setup and load paths hydrate the stored config from
the built-in one before the compatibility check, so the newly
declared slots are backfilled rather than compared.
Reorder the JSON field-table rows so the physical order of the trio
follows the forkOrder key (the trio now physically follows
TIPXDCXCancellationFeeBlock); the generator sorts the fork-order table
by the key, so this changes no output: the trio stayed the last three
rows of the common table, which is the order the semantic config digest
reads. Row order is not free elsewhere - the common table keeps the JSON
row order, and chainConfigForkBlockFields preserves it into that digest,
so reordering its rows changes it. The built-in table is emitted as the
standard fields followed by the XDC-specific ones, each group in JSON row
order, so only a move inside a group changes it. The fork-order
table's own rows carry no semantics: they were already not fully aligned
with the key before this change and still are not
(TIPXDCXMinerDisableBlock and TIPXDCXReceiverDisableBlock still follow
the Cancun..Osaka rows), so the move is readability only. Pin the PR
description's own rejection example as a test case: the scan collides
with ConstantinopleBlock first, so that is the slot the error names.
Move the trio in the Fork enum and in the label map next to the
forks they share the 38383838 height with (Denylist through
TIPTRC21Fee), so the three declarations sit together. String() and
Description() move the trio the same way, so both renderings keep
following the fork order, and TestChainConfigRenderingFollowsForkOrder
now pins the trio together with the cancellation-fee anchor it sits
after. The enum values carry no meaning: the package is reached only
through String(), and no production code persists or compares a Fork;
TestGasTierForksAreOrdered pins Gas50x before Gas2500x, which this
move does not touch. The comment above the enum is left alone: it
already describes the order as a naming registry rather than as an
activation order.
Pin the one user-visible effect, ActiveForks gaining
"Constantinople" on the three built-in networks while Localnet
stays nil, so that entry cannot drift silently.
Update the statements this change makes stale: the IsIstanbul and
IsPetersburg backward-compatibility notes in params/config_forks.go
named the three networks as configs that leave the slot unset, and
now name Localnet instead. The ADR that records the sequence check
(docs/adr/0002-require-standard-fork-sequence.md) listed the three
among the networks leaving the trio empty in its migration note,
rested a rejected option on the same list, and recorded the
fallback-versus-declared-slot rejection as out of scope; the
reorder makes that rejection disappear in the direction the entry
described and appear in the other one, so the entry is updated
rather than dropped, and its config_forks.go line anchors follow
those two comments to their new lines.
Hydration of the three declared slots is a single decision: every slot the stored
config takes a position on must already agree with the anchor the built-in
networks declare, where a missing slot reads through the cancellation-fee
fallback and an explicit null is authoritative: it is neither hydrated nor
used to veto the others. No slot moves unless all of them agree, so a stored
schedule never gains a fork-ID boundary it did not have, and the batch is
re-validated as a whole and rolled back when the full startup check
(CheckConfigForkOrder) rejects it. Fork-order repair hints follow the same
rule: a slot is only offered as droppable after clearing it and re-validating
the result, so a diagnostic cannot recommend a change the next validation
rejects.
ee21e4a to
133afc7
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Fork-order and stored-config hydration changes require maintainer confirmation of historical and private-chain upgrade compatibility.
No open issues 🎉
1 resolved since last review
1 file not reviewed
params/config_backfill_generated.go: Generated file
Review effort: Balanced


Proposed changes
XDCMainnetChainConfig,TestnetChainConfigandDevnetChainConfigleaveConstantinopleBlock,PetersburgBlockandIstanbulBlockunset. At runtimeIsPetersburgandIsIstanbulfall back toTIPXDCXCancellationFeeBlock, so those two forks activate at the cancellation-fee height (38383838 on mainnet, 23779191 on testnet, 0 on devnet), whileIsConstantinoplehas no fallback and is never true on any of the three chains. Declaring those values was rejected byCheckConfigForkOrderwithIstanbulBlock 38383838 > TIPSigningBlock 3000000: the value was right, the slot's position in the validation order was what refused it, because the slots sat before the XDC TIP forks whose activation height they share. Issue #2773 records the analysis and the escape routes.This PR declares the three slots at their effective heights and moves them after
TIPXDCXCancellationFeeBlockin the fork-order table, next to the anchor whose height they share (the slots betweenTIPSigningBlockandTIPXDCXCancellationFeeBlockactivate at or before that height on the built-in networks). The JSON rows are reordered to follow theforkOrderkey (the trio now physically followsTIPXDCXCancellationFeeBlock); the generator sorts by the key, so the row order carries no semantics: it is not a validation sequence and was already not fully aligned with the key before this change (TIPXDCXMinerDisableBlock/TIPXDCXReceiverDisableBlockstill follow the Cancun..Osaka rows); onlyforkOrderis semantic in that table. The embeddedgenesis/{mainnet,testnet,devnet}.jsongain the same three keys so theXDC init <network>path and the--<network>path stay in lockstep. In those files the keys sit next todenylistBlock, the height they share, rather than afterbyzantiumBlock: the genesis files group fields by activation height and their key order is an append history that has never matched the Go struct order. Setting all three follows the upstream convention for a skipped fork — geth mainnet declares Constantinople and Petersburg both at 7280000 — and matches the runtime fallback, so the declaration changes no behavior:GatherForks()is byte-identical before and after (hence the fork id), Istanbul rules were already active at these heights through the fallback, and the newly-trueIsConstantinopleonly adds anActiveForksentry, which is surfaced by theXDPoS_getConfigRPC and nowhere on the wire. Nodes holding a stored config from before this change need no action: the setup and load paths hydrate the stored config from the built-in one (BackfillMissingFieldsFrom) before the compatibility check, so the newly declared slots are backfilled rather than compared. Hydration of the three slots is a single decision: every slot the stored config takes a position on must already agree with the built-in height, with a missing slot read through the cancellation-fee fallback and an explicit null treated as authoritative (neither hydrated nor used to veto the others). No slot moves unless all of them agree, so a stored schedule never gains a fork-ID boundary it did not have, and the batch is re-validated as a whole and rolled back if the full startup validation (CheckConfigForkOrder) rejects it. Chains that deliberately avoid Constantinople while keeping the cancellation fee at the built-in height must therefore declareconstantinopleBlockas an explicit null.The costs are the ones #2773 predicts. Test fixtures that push the XDC forks into the future must now carry the trio together at the anchor height (
setXinFinForksToFuturein core, the engine v1/v2 test helpers,contracts/utils_test.go, the three TIPSigning-activation fixtures, and the js tracer precompile fixture), and a custom config that declares the trio at a pre-cancellation-fee height (for exampleconstantinopleBlock = petersburgBlock = 0) together with a latertipXDCXCancellationFeeBlockis rejected at startup withTIPXDCXCancellationFeeBlock 1000000000 > ConstantinopleBlock 0— the scan collides with ConstantinopleBlock first, so that is the slot the error names. The built-in Localnet config stays unfilled on purpose: it is the hydration source for custom chain configs, and filling it would plant a zero in every private chain that omits these slots.Fixes #2773.
Summary
Everything below lands in the same commit.
CheckConfigForkOrdernow names the slot to move when the order check refuses a config, in both the ordered scan and the special-case rules, and theparams/forksenum is reordered soConstantinople,PetersburgandIstanbulsit with the other38383838forks, withString()andDescription()following the same move: the enum documents itself as a naming registry for the labels returned byString(), and its numeric values carry no meaning (the package is only reached throughString()outside tests).paramsalso gains the mirror of the rejection test — a custom config that declares the trio at or after the anchor is accepted.Evidence
GatherForks()with the change stashed and reapplied is byte-identical for all three networks (mainnet[1 2 3 4 3000000 3464000 5000000 38383838 76321000 80370000 80370900 98800200 98802000], testnet[... 23779191 56828700 61290000 66825000 71550000 71551800 83600000 87633000], devnet[2700 25000 50000 1385100]), so the handshake filter accepts the same peers as before.Rules()flips the three flags true exactly at 38383838 / 23779191 / 0;IsConstantinopleis the only newly-true flag, and the instruction set and precompile set are unchanged becauseIsIstanbulis checked first at the same height.TestEmbeddedGenesisHashMatchesParams); embedded configs match the canonical params configs (TestEmbeddedGenesisConfigMatchesParams).gofmt/goimportsclean,make all,make generate(no stale files),make tidy(no module change),make quick-test(no failures), and the-shortrun over every package except./node(its test binds a hardcoded127.0.0.1:7878, occupied by a local web server on this dev box) — no failures.Merge Danger
Door: two-way for the repository, one-way for an operator whose private chain config declares any of the three slots earlier than the last declared fork that precedes them in the fork-order table. On the built-in networks that anchor is
tipXDCXCancellationFeeBlock. When that slot is unset in a bareChainConfigthe comparison anchor degrades totipXDCXLendingBlock, or whichever fork before the trio is the last one declared; on a real startup path it does not degrade, because the omitted slot is backfilled fromLocalnetChainConfigat0(BackfillMissingFieldsFrom, orBackfillCustomMigratedFieldsFromfor a config carrying anXDPoSsection), so the refusal namesTIPXDCXCancellationFeeBlock 0instead. Two shapes hit it, both grown out of the "Constantinople-era chain" intuition that #2773 records:constantinopleBlock/petersburgBlockdeclared right afterbyzantiumBlockwhile the XDC forks are scheduled much later (rejected withTIPXDCXCancellationFeeBlock 23779191 > ConstantinopleBlock 4), and the same config withtipXDCXCancellationFeeBlockdropped (rejected at startup withTIPXDCXLendingBlock 1000000000 > TIPXDCXCancellationFeeBlock 0— removing the slot does not rescue the shape: the omitted height is backfilled to0, so the refusal moves to that slot; theTIPXDCXLendingBlock 23779191 > ConstantinopleBlock 4form is what a bareChainConfigproduces). No edit both satisfies the order and preserves history: raising the trio to the anchor height, or lowering the XDC forks, changes the rules for the blocks in between, so re-validating existing blocks would diverge. The honest options for those chains are staying on the previous release or accepting that as a rule change — the same framing as #2772. A config that leaves the trio unset is not affected: nil slots are skipped by the order scan.Checking an existing chain: read the
configblock of thegenesis.jsonthe node starts with, or the chain config already persisted in the datadir, and list the declared fork heights (for a genesis file, count a key it omits as backfilled: the startup path fills the XDC fork heights fromLocalnetChainConfig, whosetipXDCXCancellationFeeBlockis0). WalktipSigningBlock,tipRandomizeBlock,tipIncreaseMasternodesBlock,denylistBlock,tipNoHalvingMNRewardBlock,tipXDCXBlock,tipXDCXLendingBlock,tipXDCXCancellationFeeBlockin that order and note the last one that is actually declared; if any ofconstantinopleBlock,petersburgBlock,istanbulBlockis declared and lower than that height, the new binary refuses to start. The refusal names the slot to move — for the example above it isTIPXDCXCancellationFeeBlock 1000000000 > ConstantinopleBlock 0: declare ConstantinopleBlock at or after TIPXDCXCancellationFeeBlock(no drop hint: Petersburg sits at the same height, so clearing only Constantinople leaves the same violation) — so the fix is reading the error. Chains that omit all three slots, and the three built-in networks, are unaffected.Overlap with #2772: #2772 (merged as
714476f966, this branch's base) marksIstanbulBlockas a required slot in the same fork-order table this PR reorders, so the two constraints stack — a config declaringistanbulBlockmust both satisfy the required-sequence scan and clear the anchor above. This branch is already based on that merge, so no rebase is pending.Upgrade path: operators of the three built-in networks, and of custom chains that omit these slots, upgrade in place — swap the binary and restart; no config change, no rewind, no re-init, because the genesis hash is unchanged and the setup and load paths backfill the slots from the built-in config (
BackfillMissingFieldsFrom) before the compatibility check, keeping a slot unset when injecting it would fail startup validation. Only operators of the shapes above have a decision to make.All five built-in networks resolve: the three XDC networks now declare the slots explicitly,
MainnetChainConfigand the unfilled Localnet are untouched.Blast Radius:
paramsstartup validation plus the three built-in configs and three embedded genesis files; the fallout in tests is the fixtures that push the XDC forks into the future (eight sites across eight files, the trio added or raised to the anchor height) and the three golden chain-config semantic hashes. New tests pin the declared heights of the three built-in networks (Localnet staying nil), the unchangedGatherForks()output for all three, and the three config shapes the new position refuses (including the example above), plus theActiveForksentry the newly-trueIsConstantinopleadds.Types of changes
What types of changes does your code introduce to XDC network?
Put an
✅in the boxes that applyImpacted Components
Which parts of the codebase does this PR touch?
Put an
✅in the boxes that applyChecklist
Put an
✅in the boxes once you have confirmed below actions (or provide reasons on not doing so) thatdocs/adr/0002-require-standard-fork-sequence.mdrecords what this change makes stale: its migration note, the rejected option that rested on the built-in networks' shape, and the out-of-scope entry for the fallback-versus-declared-slot rejection.