From f5700002f8be3fb48c7b0bb0e014203df4822c9d Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 08:07:43 +0800 Subject: [PATCH 1/9] docs(upgrade): normalise the wrapping and structure of the upgrade notes The file mixed hard-wrapped paragraphs with single-line ones, so editing it meant re-wrapping by hand, and every later change to a wrapped paragraph showed up as a diff across the lines it happened to span. Put every paragraph, bullet and numbered item on its own line; 187 continuation lines are joined. That same pass restores structure the wrapping had flattened. Nine bullets in the presence-tracking list carried their four labelled sub-items as indented text on the bullet's own line, and the same shape appears in three groups further down; those labelled lines are now sub-items nested under the parent they describe. The six steps of the chain-config upgrade procedure, which the wrapping had run together into one paragraph around two commands, are a list, and the sentence that introduces the second command stays plain text inside the item it belongs to rather than becoming an item of its own. The runtime choices for the four networks named each network twice, once for its flag and once for its notes; each network is one bullet now, with its startup flag and its notes as sub-items. Existing fenced blocks, tables and list indentation are untouched. This is not a whitespace-only change: the list markers are new. --- docs/upgrade.md | 407 +++++++++++++++--------------------------------- 1 file changed, 129 insertions(+), 278 deletions(-) diff --git a/docs/upgrade.md b/docs/upgrade.md index 5830f0892281..501196a43bf2 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -1,7 +1,6 @@ # Upgrade Notes -This document summarizes the current startup rules for `genesis` and -`ChainConfig` in XDPoSChain: +This document summarizes the current startup rules for `genesis` and `ChainConfig` in XDPoSChain: - how single-binary startup now selects the target network at runtime - how the node distinguishes built-in networks, Localnet, and custom networks @@ -12,10 +11,7 @@ This document summarizes the current startup rules for `genesis` and ## Operator Migration: Single Binary, Runtime Network Selection -The network-specific constant files `constants.mainnet.go`, -`constants.testnet.go`, `constants.devnet.go`, and `constants.local.go` have -been removed from the startup path. Operators should now assume a single `XDC` -binary and select the target network at runtime. +The network-specific constant files `constants.mainnet.go`, `constants.testnet.go`, `constants.devnet.go`, and `constants.local.go` have been removed from the startup path. Operators should now assume a single `XDC` binary and select the target network at runtime. Impact for existing automation: @@ -25,14 +21,21 @@ Impact for existing automation: Built-in network selection now maps to the following runtime choices: -- Mainnet: `XDC --mainnet ...` -- Mainnet notes: equivalent alias `--xinfin`. If `--networkid` is omitted, startup uses chain ID `50` and the built-in mainnet genesis. -- Testnet: `XDC --testnet ...` -- Testnet notes: equivalent alias `--apothem`. If `--networkid` is omitted, startup uses chain ID `51` and the built-in testnet genesis. -- Devnet: `XDC --devnet ...` -- Devnet notes: if `--networkid` is omitted, startup uses chain ID `551` and the built-in devnet genesis. -- Localnet: explicit `genesis.json` plus `XDC --datadir init /path/to/genesis.json`, then `XDC --datadir --networkid 5151 ...` -- Localnet notes: there is no dedicated `--localnet` flag. Localnet is identified from the resolved config by `ChainID == 5151`. +- Mainnet: + - Startup: `XDC --mainnet ...`. + - Equivalent alias `--xinfin`. + - If `--networkid` is omitted, startup uses chain ID `50` and the built-in mainnet genesis. +- Testnet: + - Startup: `XDC --testnet ...`. + - Equivalent alias `--apothem`. + - If `--networkid` is omitted, startup uses chain ID `51` and the built-in testnet genesis. +- Devnet: + - Startup: `XDC --devnet ...`. + - If `--networkid` is omitted, startup uses chain ID `551` and the built-in devnet genesis. +- Localnet: + - Startup: explicit `genesis.json` plus `XDC --datadir init /path/to/genesis.json`, then `XDC --datadir --networkid 5151 ...`. + - There is no dedicated `--localnet` flag. + - Localnet is identified from the resolved config by `ChainID == 5151`. Practical migration rules: @@ -58,9 +61,7 @@ XDC --datadir init /path/to/genesis.json XDC --datadir --networkid 5151 ``` -If an operator script previously inferred the network only from which constant -file or binary variant was present, that script now needs an explicit runtime -branch. The required branch point is: +If an operator script previously inferred the network only from which constant file or binary variant was present, that script now needs an explicit runtime branch. The required branch point is: - built-in networks: add the matching built-in flag - Localnet/custom networks: pass the authoritative `genesis.json` on writable initialization and use the intended chain ID on normal startup @@ -78,85 +79,67 @@ The node classifies the effective startup config in the following order: ## Backfill Rules -Backfill only applies to fields that are actually missing. Explicit `0`, -`false`, `null`, or zero-address values remain authoritative and are not -overwritten. However, a zero address is only treated as an explicit override -for backfill purposes. If validation requires a system-contract address to be -set, the config may still be rejected instead of accepting the zero address as -a usable value. - -`BackfillMissingFieldsFrom` uses strict JSON-key presence when the config came -from `UnmarshalJSON`. In that case, an explicit value such as -`"berlinBlock": 0` or `"pragueBlock": null` is preserved because the key is -present in the original JSON. If a `ChainConfig` is constructed directly in Go -code, or through a helper that never populated JSON presence metadata, the -compatibility fallback degrades to treating `nil` pointers and zero values as -missing. That fallback is intentional for legacy callers, but it means `nil` -on a fork-block pointer is interpreted as "missing", not as "this fork is -intentionally absent". - -Presence tracking is not uniform across every field class. Use the list below -when deciding whether omission, `null`, `0`, `false`, or a zero address will be -preserved during backfill. +Backfill only applies to fields that are actually missing. Explicit `0`, `false`, `null`, or zero-address values remain authoritative and are not overwritten. However, a zero address is only treated as an explicit override for backfill purposes. If validation requires a system-contract address to be set, the config may still be rejected instead of accepting the zero address as a usable value. + +`BackfillMissingFieldsFrom` uses strict JSON-key presence when the config came from `UnmarshalJSON`. In that case, an explicit value such as `"berlinBlock": 0` or `"pragueBlock": null` is preserved because the key is present in the original JSON. If a `ChainConfig` is constructed directly in Go code, or through a helper that never populated JSON presence metadata, the compatibility fallback degrades to treating `nil` pointers and zero values as missing. That fallback is intentional for legacy callers, but it means `nil` on a fork-block pointer is interpreted as "missing", not as "this fork is intentionally absent". + +Presence tracking is not uniform across every field class. Use the list below when deciding whether omission, `null`, `0`, `false`, or a zero address will be preserved during backfill. - Top-level `*big.Int` fork pointers - Examples: `berlinBlock`, `pragueBlock`, `tip2019Block` - JSON-backed configs: yes. `0` and `null` are both preserved because the JSON key is tracked. - After `CloneForBackfill` on programmatic configs: yes. Any non-`nil` pointer is treated as explicit, including `big.NewInt(0)`. - Backfill meaning: `*big.Int(0)` means "active from genesis"; `nil` means missing when no JSON presence is available. + - Examples: `berlinBlock`, `pragueBlock`, `tip2019Block` + - JSON-backed configs: yes. `0` and `null` are both preserved because the JSON key is tracked. + - After `CloneForBackfill` on programmatic configs: yes. Any non-`nil` pointer is treated as explicit, including `big.NewInt(0)`. + - Backfill meaning: `*big.Int(0)` means "active from genesis"; `nil` means missing when no JSON presence is available. - Top-level consensus-engine pointers - Examples: `ethash`, `clique`, `XDPoS` - JSON-backed configs: yes. - After `CloneForBackfill` on programmatic configs: yes. - Backfill meaning: `nil` means missing, non-`nil` means explicit. + - Examples: `ethash`, `clique`, `XDPoS` + - JSON-backed configs: yes. + - After `CloneForBackfill` on programmatic configs: yes. + - Backfill meaning: `nil` means missing, non-`nil` means explicit. - Top-level system-contract addresses - Examples: `trc21IssuerSMC` - JSON-backed configs: yes. A JSON zero address is preserved as an explicit override. - After `CloneForBackfill` on programmatic configs: partially. Only non-zero addresses can be inferred as present. - Backfill meaning: zero address is authoritative only when JSON key presence was captured. + - Examples: `trc21IssuerSMC` + - JSON-backed configs: yes. A JSON zero address is preserved as an explicit override. + - After `CloneForBackfill` on programmatic configs: partially. Only non-zero addresses can be inferred as present. + - Backfill meaning: zero address is authoritative only when JSON key presence was captured. - Top-level booleans - Examples: `daoForkSupport` - JSON-backed configs: yes. - After `CloneForBackfill` on programmatic configs: partially. `true` is inferrable; `false` needs JSON presence unless related fork context already proves intent. - Backfill meaning: without presence tracking, `false` falls back to "missing". + - Examples: `daoForkSupport` + - JSON-backed configs: yes. + - After `CloneForBackfill` on programmatic configs: partially. `true` is inferrable; `false` needs JSON presence unless related fork context already proves intent. + - Backfill meaning: without presence tracking, `false` falls back to "missing". - XDPoS scalar numerics - Examples: `period`, `epoch`, `reward`, `maxMasternodesV2` - JSON-backed configs: yes. - After `CloneForBackfill` on programmatic configs: partially. Non-zero values are inferrable; zero needs JSON presence. - Backfill meaning: without presence tracking, zero falls back to "missing". + - Examples: `period`, `epoch`, `reward`, `maxMasternodesV2` + - JSON-backed configs: yes. + - After `CloneForBackfill` on programmatic configs: partially. Non-zero values are inferrable; zero needs JSON presence. + - Backfill meaning: without presence tracking, zero falls back to "missing". - XDPoS boolean - Examples: `SkipV1Validation` - JSON-backed configs: yes. - After `CloneForBackfill` on programmatic configs: yes for `CloneForBackfill`; it explicitly tracks this field even when `false`. - Backfill meaning: `false` stays authoritative once tracked. + - Examples: `SkipV1Validation` + - JSON-backed configs: yes. + - After `CloneForBackfill` on programmatic configs: yes for `CloneForBackfill`; it explicitly tracks this field even when `false`. + - Backfill meaning: `false` stays authoritative once tracked. - V2 scalar numerics and floats - Examples: `switchRound`, `timeoutPeriod`, `certificateThreshold`, `base` - JSON-backed configs: yes. - After `CloneForBackfill` on programmatic configs: partially. Non-zero values are inferrable; zero needs JSON presence. - Backfill meaning: without presence tracking, zero falls back to "missing". + - Examples: `switchRound`, `timeoutPeriod`, `certificateThreshold`, `base` + - JSON-backed configs: yes. + - After `CloneForBackfill` on programmatic configs: partially. Non-zero values are inferrable; zero needs JSON presence. + - Backfill meaning: without presence tracking, zero falls back to "missing". - Nested pointer fields - Examples: `XDPoS.v2.switchBlock`, `XDPoS.v2.config`, `XDPoS.v2.expTimeoutConfig` - JSON-backed configs: yes. - After `CloneForBackfill` on programmatic configs: yes for non-`nil` pointers or populated nested structs. - Backfill meaning: `nil` stays authoritative only when the containing JSON key was present. + - Examples: `XDPoS.v2.switchBlock`, `XDPoS.v2.config`, `XDPoS.v2.expTimeoutConfig` + - JSON-backed configs: yes. + - After `CloneForBackfill` on programmatic configs: yes for non-`nil` pointers or populated nested structs. + - Backfill meaning: `nil` stays authoritative only when the containing JSON key was present. - Container fields with entry-level limits - Examples: `XDPoS.v2.allConfigs` - JSON-backed configs: container presence is tracked, and explicit `allConfigs[round]: null` entries are preserved once decoded into the map. - After `CloneForBackfill` on programmatic configs: container presence is tracked when the map is non-empty, but per-entry scalar zero values still rely on each nested config's own tracking. - Backfill meaning: backfill only adds missing map keys; it does not overwrite an existing key whose value is `null`. + - Examples: `XDPoS.v2.allConfigs` + - JSON-backed configs: container presence is tracked, and explicit `allConfigs[round]: null` entries are preserved once decoded into the map. + - After `CloneForBackfill` on programmatic configs: container presence is tracked when the map is non-empty, but per-entry scalar zero values still rely on each nested config's own tracking. + - Backfill meaning: backfill only adds missing map keys; it does not overwrite an existing key whose value is `null`. -The practical rule is: if a field needs to distinguish "explicit zero/false/null" -from "missing", prefer JSON input or call `CloneForBackfill` before hydration. -For pointer-valued fork fields, `CloneForBackfill` is sufficient to preserve the -`*big.Int(0)` vs `nil` distinction. +The practical rule is: if a field needs to distinguish "explicit zero/false/null" from "missing", prefer JSON input or call `CloneForBackfill` before hydration. For pointer-valued fork fields, `CloneForBackfill` is sufficient to preserve the `*big.Int(0)` vs `nil` distinction. | Class | Backfill source | What is backfilled | | ------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -190,38 +173,23 @@ Prague declaration requirement: The node does not rewrite the database on every startup. -1. On an empty database, first initialization writes the resolved - `ChainConfig` after startup hydration and validation. -2. On an existing database, ordinary startup compares the stored config and the - new effective config after clearing JSON field-presence metadata. -3. If the two effective configs are semantically identical, startup does not - rewrite the stored chain-config blob just because runtime backfill filled - omitted fields in memory. -4. If the new effective config is semantically different and compatibility - checks pass, the node writes the new resolved `ChainConfig` to the database. +1. On an empty database, first initialization writes the resolved `ChainConfig` after startup hydration and validation. +2. On an existing database, ordinary startup compares the stored config and the new effective config after clearing JSON field-presence metadata. +3. If the two effective configs are semantically identical, startup does not rewrite the stored chain-config blob just because runtime backfill filled omitted fields in memory. +4. If the new effective config is semantically different and compatibility checks pass, the node writes the new resolved `ChainConfig` to the database. -In this context, “semantically different” means the two configs differ after -ignoring JSON field-presence tracking. In other words, field omission alone is -not treated as a meaningful change if the resolved values are the same. +In this context, “semantically different” means the two configs differ after ignoring JSON field-presence tracking. In other words, field omission alone is not treated as a meaningful change if the resolved values are the same. Additional notes: -- For built-in networks, adding a new future fork in `params/config.go` does - not automatically rewrite an existing stored built-in `ChainConfig` during an - ordinary restart if runtime hydration already makes the effective config match - the bundled config. -- For same-hash custom chains, first explicit initialization can also persist a - chain-config override marker for that data directory. +- For built-in networks, adding a new future fork in `params/config.go` does not automatically rewrite an existing stored built-in `ChainConfig` during an ordinary restart if runtime hydration already makes the effective config match the bundled config. +- For same-hash custom chains, first explicit initialization can also persist a chain-config override marker for that data directory. ## Chain Config Mismatch Policy (`--chain-config-mismatch-policy`) -When startup resolves a runtime `ChainConfig` that is incompatible with the -stored config, `SetupGenesisBlock` / `LoadChainConfigWithCompat` return a -non-nil `ConfigCompatError`. How the node reacts to that error is now controlled -by an explicit startup policy instead of an unconditional rewind. +When startup resolves a runtime `ChainConfig` that is incompatible with the stored config, `SetupGenesisBlock` / `LoadChainConfigWithCompat` return a non-nil `ConfigCompatError`. How the node reacts to that error is now controlled by an explicit startup policy instead of an unconditional rewind. -Set the policy with the `--chain-config-mismatch-policy` flag (or the -`ChainConfigMismatchPolicy` field in the TOML config). Supported values: +Set the policy with the `--chain-config-mismatch-policy` flag (or the `ChainConfigMismatchPolicy` field in the TOML config). Supported values: - `exit` (default) - Behavior on mismatch: abort startup with an error; the database is not modified. @@ -247,32 +215,15 @@ Set the policy with the `--chain-config-mismatch-policy` flag (or the Behavior change relative to older startup logic: -- The previous behavior was equivalent to `rewind-and-update` and ran - unconditionally on writable startup. The default is now `exit`, so an - incompatible config makes the node stop and hand the decision to the operator - instead of silently rewinding the chain and rewriting stored config. -- To preserve the old automatic-rewind behavior, start with - `--chain-config-mismatch-policy=rewind-and-update`. +- The previous behavior was equivalent to `rewind-and-update` and ran unconditionally on writable startup. The default is now `exit`, so an incompatible config makes the node stop and hand the decision to the operator instead of silently rewinding the chain and rewriting stored config. +- To preserve the old automatic-rewind behavior, start with `--chain-config-mismatch-policy=rewind-and-update`. Operational guidance: -- Prefer `exit` (the default) and investigate why the resolved config disagrees - with the stored config before choosing a recovery mode. A mismatch usually - means the wrong `--networkid`/`--datadir`/`genesis.json` combination, not a - routine upgrade. -- Use `rewind-and-update` only when you intend to roll the head back to the - fork boundary and reprocess blocks under the new rules. This is the only - policy that keeps the head consistent with the new config. -- `update-config-only` and `ignore-mismatch` are advanced escape hatches and **high-risk options**. They do **not** - reprocess blocks that were already imported past the changed fork boundary, - so the running head can diverge in state/consensus from a node that rewound. - `update-config-only` additionally marks the mismatch as resolved on disk, so the - warning will not recur even though no reprocessing happened. Treat both as - `may cause state/consensus divergence; expert use only` and use them only - when you fully understand the consensus implications. -- `rewind-and-update` and `update-config-only` require a writable open. In readonly - mode they refuse to start so the database is never mutated; reopen in writable - mode, or use `exit`/`ignore-mismatch`, to proceed without writes. +- Prefer `exit` (the default) and investigate why the resolved config disagrees with the stored config before choosing a recovery mode. A mismatch usually means the wrong `--networkid`/`--datadir`/`genesis.json` combination, not a routine upgrade. +- Use `rewind-and-update` only when you intend to roll the head back to the fork boundary and reprocess blocks under the new rules. This is the only policy that keeps the head consistent with the new config. +- `update-config-only` and `ignore-mismatch` are advanced escape hatches and **high-risk options**. They do **not** reprocess blocks that were already imported past the changed fork boundary, so the running head can diverge in state/consensus from a node that rewound. `update-config-only` additionally marks the mismatch as resolved on disk, so the warning will not recur even though no reprocessing happened. Treat both as `may cause state/consensus divergence; expert use only` and use them only when you fully understand the consensus implications. +- `rewind-and-update` and `update-config-only` require a writable open. In readonly mode they refuse to start so the database is never mutated; reopen in writable mode, or use `exit`/`ignore-mismatch`, to proceed without writes. ## Startup API Semantics @@ -292,30 +243,21 @@ Important behavior changes relative to older startup logic: ## Schema Change: `chainConfigOverride` -Same-hash custom chains now persist a dedicated metadata marker under the rawdb -key prefix: +Same-hash custom chains now persist a dedicated metadata marker under the rawdb key prefix: ```text ethereum-cfgoverride- ``` -The value is a versioned payload written by `WriteChainConfigOverride` when a -data directory is intentionally using a custom `ChainConfig` for a genesis hash -that also matches a bundled built-in network. The current format is two bytes: +The value is a versioned payload written by `WriteChainConfigOverride` when a data directory is intentionally using a custom `ChainConfig` for a genesis hash that also matches a bundled built-in network. The current format is two bytes: ```text {version=1, flags=1} ``` -This change does not bump `core.BlockChainVersion`. The marker is additive -metadata under a new rawdb key prefix; it does not rewrite existing block, -receipt, trie, or chain-config encodings, and it does not change the meaning of -`databaseVersionKey`. Older binaries that do not know this key simply leave it -unread. +This change does not bump `core.BlockChainVersion`. The marker is additive metadata under a new rawdb key prefix; it does not rewrite existing block, receipt, trie, or chain-config encodings, and it does not change the meaning of `databaseVersionKey`. Older binaries that do not know this key simply leave it unread. -The override prefix intentionally no longer shares the `ethereum-config-` -prefix namespace, so future rawdb prefix iteration over chain-config blobs does -not accidentally scan override metadata. +The override prefix intentionally no longer shares the `ethereum-config-` prefix namespace, so future rawdb prefix iteration over chain-config blobs does not accidentally scan override metadata. This marker is used to distinguish: @@ -331,9 +273,7 @@ Forward-compatibility rules: Downgrade warning: - Older binaries do not know about the versioned `chainConfigOverride` metadata. -- At the rawdb layer they ignore the additional key, but because they never - consult the marker they cannot preserve the upgraded same-hash custom-chain - classification rules. +- At the rawdb layer they ignore the additional key, but because they never consult the marker they cannot preserve the upgraded same-hash custom-chain classification rules. - If you start a new binary and allow it to persist the new marker, then roll back to an older binary on the same data directory, the old binary may misclassify the chain instead of refusing startup. - For a same-hash custom chain, that downgrade can surface `errGenesisConfigConflict` or cause the node to ignore the intended custom classification because the old code cannot see the override marker. @@ -344,8 +284,7 @@ Operational guidance: ## Compatibility Matrix For Existing Mainnet/Testnet/Devnet Databases -This matrix applies to data directories whose canonical genesis hash is one of -the bundled built-in networks: +This matrix applies to data directories whose canonical genesis hash is one of the bundled built-in networks: - Mainnet (`ChainID == 50`) - Testnet (`ChainID == 51`) @@ -359,45 +298,24 @@ Use the matching runtime flag on the upgraded binary: Ordinary built-in Mainnet, Testnet, or Devnet database using the bundled config - First startup on the current binary: - Supported as a direct restart with the matching built-in runtime flag. No explicit `genesis.json` is required for ordinary built-in data directories. - - State after successful upgrade or repair: - Remains a built-in network database. Ordinary startup does not need to persist `chainConfigOverride` metadata for this path. - - Can the same post-upgrade database be reopened by an older binary? - Usually yes. The additive override-marker schema is not required for an ordinary built-in database, so rollback risk is much lower. Still take a backup before any downgrade. - - Operator guidance: - A normal rolling upgrade is acceptable. Keep the network selection explicit with `--mainnet`, `--testnet`, or `--devnet`. + - First startup on the current binary: Supported as a direct restart with the matching built-in runtime flag. No explicit `genesis.json` is required for ordinary built-in data directories. + - State after successful upgrade or repair: Remains a built-in network database. Ordinary startup does not need to persist `chainConfigOverride` metadata for this path. + - Can the same post-upgrade database be reopened by an older binary? Usually yes. The additive override-marker schema is not required for an ordinary built-in database, so rollback risk is much lower. Still take a backup before any downgrade. + - Operator guidance: A normal rolling upgrade is acceptable. Keep the network selection explicit with `--mainnet`, `--testnet`, or `--devnet`. Legacy pre-marker same-hash custom database on a built-in genesis hash or chain ID, with no persisted override marker yet - First startup on the current binary: - Not safe as a plain restart. Readonly checks and restart attempts without the authoritative custom `genesis.json` can fail with `errGenesisConfigConflict`. The current binary does not implicitly migrate this case on a readonly path. - - State after successful upgrade or repair: - After a writable repair, typically `XDC --allow-builtin-config-override init --datadir /path/to/genesis.json` or another writable startup with the matching authoritative genesis, the node persists the repaired chain-config metadata and the `chainConfigOverride` marker. Later readonly reopen paths on the current binary can then use the stored custom classification directly; writable restarts that would repair or rewrite metadata still require `--allow-builtin-config-override`. - - Can the same post-upgrade database be reopened by an older binary? - No, not safely after the repair has written the marker. Older binaries ignore the marker and may misclassify the chain or surface a config conflict. - - Operator guidance: - Treat the first upgraded start as a migration. Back up the data directory, run the writable repair with the exact matching custom genesis and `--allow-builtin-config-override`, then use readonly commands normally once that migration has completed. Keep `--allow-builtin-config-override` on later writable repair or rewrite paths that must continue to honor the stored custom override instead of the bundled built-in config. + - First startup on the current binary: Not safe as a plain restart. Readonly checks and restart attempts without the authoritative custom `genesis.json` can fail with `errGenesisConfigConflict`. The current binary does not implicitly migrate this case on a readonly path. + - State after successful upgrade or repair: After a writable repair, typically `XDC --allow-builtin-config-override init --datadir /path/to/genesis.json` or another writable startup with the matching authoritative genesis, the node persists the repaired chain-config metadata and the `chainConfigOverride` marker. Later readonly reopen paths on the current binary can then use the stored custom classification directly; writable restarts that would repair or rewrite metadata still require `--allow-builtin-config-override`. + - Can the same post-upgrade database be reopened by an older binary? No, not safely after the repair has written the marker. Older binaries ignore the marker and may misclassify the chain or surface a config conflict. + - Operator guidance: Treat the first upgraded start as a migration. Back up the data directory, run the writable repair with the exact matching custom genesis and `--allow-builtin-config-override`, then use readonly commands normally once that migration has completed. Keep `--allow-builtin-config-override` on later writable repair or rewrite paths that must continue to honor the stored custom override instead of the bundled built-in config. Already-migrated same-hash custom database on a built-in genesis hash or chain ID, with `chainConfigOverride` already present - First startup on the current binary: - Supported on the current binary. The marker preserves same-hash custom classification for that data directory, so readonly reopen paths use the stored custom config directly. Writable startup paths that may rewrite metadata still require `--allow-builtin-config-override`. - - State after successful upgrade or repair: - Remains an override-backed same-hash custom database. Current binaries continue to honor the stored custom classification and repaired metadata. - - Can the same post-upgrade database be reopened by an older binary? - No, not safely. This is the downgrade case called out above: older binaries do not understand `chainConfigOverride` and cannot preserve the upgraded classification rules. - - Operator guidance: - Do not downgrade on the same live database. If an older binary must be tested, restore a backup or snapshot taken before the marker existed. + - First startup on the current binary: Supported on the current binary. The marker preserves same-hash custom classification for that data directory, so readonly reopen paths use the stored custom config directly. Writable startup paths that may rewrite metadata still require `--allow-builtin-config-override`. + - State after successful upgrade or repair: Remains an override-backed same-hash custom database. Current binaries continue to honor the stored custom classification and repaired metadata. + - Can the same post-upgrade database be reopened by an older binary? No, not safely. This is the downgrade case called out above: older binaries do not understand `chainConfigOverride` and cannot preserve the upgraded classification rules. + - Operator guidance: Do not downgrade on the same live database. If an older binary must be tested, restore a backup or snapshot taken before the marker existed. Operational summary: @@ -407,8 +325,7 @@ Operational summary: ## Operator Impact and Repair Workflow -Readonly checks can now tell you that the database is logically repairable but -not safe to continue with as-is. +Readonly checks can now tell you that the database is logically repairable but not safe to continue with as-is. Typical cases: @@ -416,14 +333,9 @@ Typical cases: - a same-hash custom chain is missing its stored config blob and must be reopened with an explicit genesis on a writable path - writable startup needs to persist the new override marker for a legacy pre-marker same-hash custom chain -In those cases, the correct response is to reopen the data directory in a -writable mode using the current binary and repair the metadata, rather than to -keep retrying readonly startup. +In those cases, the correct response is to reopen the data directory in a writable mode using the current binary and repair the metadata, rather than to keep retrying readonly startup. -For a legacy pre-marker same-hash custom database, do not expect an ordinary -restart or a readonly command to perform that migration implicitly. The repair -must be done on a writable path with the authoritative matching `genesis.json` -and explicit `--allow-builtin-config-override`. +For a legacy pre-marker same-hash custom database, do not expect an ordinary restart or a readonly command to perform that migration implicitly. The repair must be done on a writable path with the authoritative matching `genesis.json` and explicit `--allow-builtin-config-override`. Recommended operator workflow: @@ -434,15 +346,11 @@ Recommended operator workflow: ## Minimal Upgrade Checklist For Strict XDC Fork Config Validation -Nodes now reject any chain config that enables XDPoS or any XDC-specific fork -field unless the following strict required fields are resolved to non-zero -usable values. +Nodes now reject any chain config that enables XDPoS or any XDC-specific fork field unless the following strict required fields are resolved to non-zero usable values. Operator checklist before the first restart on the upgraded binary: -- Confirm which authoritative source you will trust for the repair: - the persisted chain-config blob in the data directory, or the external - `genesis.json` used for `XDC init` or writable repair. +- Confirm which authoritative source you will trust for the repair: the persisted chain-config blob in the data directory, or the external `genesis.json` used for `XDC init` or writable repair. - Confirm that source explicitly declares every required field below. Top-level XDC fork and system-contract fields: @@ -470,45 +378,25 @@ XDPoS v2 fields: Recommended operator check while filling the list: -- Treat every blank, omitted key, `null`, `0`, or zero-address value as a stop - signal until you have confirmed that the current validation path really - allows it. -- If startup is expected to run with XDPoS enabled, verify that the - authoritative source contains the full `XDPoS` object, not only selected - nested fields. +- Treat every blank, omitted key, `null`, `0`, or zero-address value as a stop signal until you have confirmed that the current validation path really allows it. +- If startup is expected to run with XDPoS enabled, verify that the authoritative source contains the full `XDPoS` object, not only selected nested fields. -For custom or private networks, do not rely on omission as an upgrade strategy. -If the persisted config is incomplete, update the `config` section in the -authoritative `genesis.json` so the writable repair path can persist the full -strict-validation field set. +For custom or private networks, do not rely on omission as an upgrade strategy. If the persisted config is incomplete, update the `config` section in the authoritative `genesis.json` so the writable repair path can persist the full strict-validation field set. -For legacy custom XDPoS networks, older sparse genesis files can still pass -startup migration when those keys were omitted entirely. In that case -`hydrateLegacyCompatibleCustomChainConfig` backfills the historical migrated -fields from `params.LocalnetChainConfig` before strict validation runs. +For legacy custom XDPoS networks, older sparse genesis files can still pass startup migration when those keys were omitted entirely. In that case `hydrateLegacyCompatibleCustomChainConfig` backfills the historical migrated fields from `params.LocalnetChainConfig` before strict validation runs. Important caveat: - Auto-hydration only applies to omitted fields. -- Explicit `0`, `null`, or zero-address values remain authoritative and will - still fail strict validation if the field is required. -- The narrow custom-network backfill only runs when the config already has an - `XDPoS` section. If `config.XDPoS` is missing entirely, writable startup - cannot synthesize the missing nested XDPoS fields for you; the - authoritative `genesis.json` must declare them explicitly. +- Explicit `0`, `null`, or zero-address values remain authoritative and will still fail strict validation if the field is required. +- The narrow custom-network backfill only runs when the config already has an `XDPoS` section. If `config.XDPoS` is missing entirely, writable startup cannot synthesize the missing nested XDPoS fields for you; the authoritative `genesis.json` must declare them explicitly. Minimal `genesis.json` example for a custom XDPoS chain: -- Use this as a shape reference for the strict-validation fields, not as a - production template. -- Replace the addresses, `chainId`, fork heights, and reward values with the - authoritative values for your network. -- The fork block values shown as `0` only demonstrate that the field must be - declared. They are not universally valid defaults for an existing private - chain. -- If the chain is already initialized, merge these `config` fields into your - existing `genesis.json`; do not change other genesis fields unless you - intentionally want a different genesis hash. +- Use this as a shape reference for the strict-validation fields, not as a production template. +- Replace the addresses, `chainId`, fork heights, and reward values with the authoritative values for your network. +- The fork block values shown as `0` only demonstrate that the field must be declared. They are not universally valid defaults for an existing private chain. +- If the chain is already initialized, merge these `config` fields into your existing `genesis.json`; do not change other genesis fields unless you intentionally want a different genesis hash. ```json { @@ -588,28 +476,19 @@ Minimal `genesis.json` example for a custom XDPoS chain: } ``` -If the first upgraded startup fails with `missing fork switch` for one of the -fields above, treat it as a migration task rather than a transient startup -error. Reopen the data directory through a writable path and ensure the -persisted config or external `genesis.json` carries the required values. +If the first upgraded startup fails with `missing fork switch` for one of the fields above, treat it as a migration task rather than a transient startup error. Reopen the data directory through a writable path and ensure the persisted config or external `genesis.json` carries the required values. -Do not treat a readonly compatibility warning as a harmless cosmetic diff. It -means the writable path would need to repair metadata or require an explicit -rewind decision before startup should proceed. The recovery mode for that -rewind decision is selected with `--chain-config-mismatch-policy` (see -[Chain Config Mismatch Policy](#chain-config-mismatch-policy---chain-config-mismatch-policy)). +Do not treat a readonly compatibility warning as a harmless cosmetic diff. It means the writable path would need to repair metadata or require an explicit rewind decision before startup should proceed. The recovery mode for that rewind decision is selected with `--chain-config-mismatch-policy` (see [Chain Config Mismatch Policy](#chain-config-mismatch-policy---chain-config-mismatch-policy)). ## Same-Hash Custom Chains on Built-In IDs (`50` / `51` / `551`) -The most error-prone migration path is a custom genesis that intentionally -reuses the same block contents and built-in identity surface as: +The most error-prone migration path is a custom genesis that intentionally reuses the same block contents and built-in identity surface as: - Mainnet (`ChainID == 50`) - Testnet (`ChainID == 51`) - Devnet (`ChainID == 551`) -For these chains, classification now depends on more than the genesis hash -alone. +For these chains, classification now depends on more than the genesis hash alone. Practical rules: @@ -633,43 +512,26 @@ Minimal repair sequence for a legacy pre-marker same-hash custom database: 3. Let the current binary persist the repaired chain-config metadata and override marker. 4. After that writable migration completes, readonly commands can reopen the database normally. Use `--allow-builtin-config-override` again only for later writable repair or rewrite paths. -The helper logic behind `isLegacyStoredCustomBuiltInConfig` and -`shouldAllowCustomBuiltInConfig` is intentionally conservative. Its purpose is -to preserve legitimate legacy custom deployments, not to guess operator intent -from incomplete metadata. When in doubt, prefer an explicit writable repair -with the authoritative custom genesis file. +The helper logic behind `isLegacyStoredCustomBuiltInConfig` and `shouldAllowCustomBuiltInConfig` is intentionally conservative. Its purpose is to preserve legitimate legacy custom deployments, not to guess operator intent from incomplete metadata. When in doubt, prefer an explicit writable repair with the authoritative custom genesis file. ## Upgrading ChainConfig -Use this procedure when the chain is already running and you want to add a new -future fork or otherwise change `ChainConfig` without changing the canonical -genesis block. +Use this procedure when the chain is already running and you want to add a new future fork or otherwise change `ChainConfig` without changing the canonical genesis block. ### Prerequisites - Back up the data directory and record the current head block number. -- Start from the current effective custom or built-in config, not from an old - template. -- Keep the genesis block contents unchanged. Do not modify alloc, nonce, - timestamp, extra data, gas limit, difficulty, or any other field that would - change the genesis hash. +- Start from the current effective custom or built-in config, not from an old template. +- Keep the genesis block contents unchanged. Do not modify alloc, nonce, timestamp, extra data, gas limit, difficulty, or any other field that would change the genesis hash. - Change only future-compatible values in the `config` section. -- For same-hash custom chains, the data directory must already be an - override-backed same-hash custom chain. Otherwise the node still treats that - hash as a bundled network. +- For same-hash custom chains, the data directory must already be an override-backed same-hash custom chain. Otherwise the node still treats that hash as a bundled network. ### Steps -a. Update the `config` section in your `genesis.json` with the new future fork - switch or other future-only config value. -b. Make sure all required migrated fields, system-contract addresses, and - XDPoS settings remain explicitly declared, including - `XDPoS.FoundationWalletAddr`, `XDPoS.MaxMasternodesV2`, - `XDPoS.V2.SwitchBlock`, `XDPoS.V2.CurrentConfig`, and - `XDPoS.V2.AllConfigs`. -c. Verify that each new or modified fork switch is still in the future and - that fork ordering remains valid. -d. Apply the update with: +- Update the `config` section in your `genesis.json` with the new future fork switch or other future-only config value. +- Make sure all required migrated fields, system-contract addresses, and XDPoS settings remain explicitly declared, including `XDPoS.FoundationWalletAddr`, `XDPoS.MaxMasternodesV2`, `XDPoS.V2.SwitchBlock`, `XDPoS.V2.CurrentConfig`, and `XDPoS.V2.AllConfigs`. +- Verify that each new or modified fork switch is still in the future and that fork ordering remains valid. +- Apply the update with: ```bash XDC --datadir init /path/to/genesis.json @@ -681,29 +543,18 @@ d. Apply the update with: XDC --allow-builtin-config-override --datadir init /path/to/genesis.json ``` -e. Restart the node normally. - For same-hash custom chains on built-in IDs, readonly reopen paths can restart normally once the override marker is already persisted. Use `--allow-builtin-config-override` only if this restart is a writable repair or metadata-rewrite path. -f. Verify that the node is now using the expected updated chain config. +- Restart the node normally. For same-hash custom chains on built-in IDs, readonly reopen paths can restart normally once the override marker is already persisted. Use `--allow-builtin-config-override` only if this restart is a writable repair or metadata-rewrite path. +- Verify that the node is now using the expected updated chain config. ### Important Notes -- Running `XDC init` on a non-empty data directory does not recreate the - canonical genesis block when the genesis hash is unchanged. It re-runs - startup chain-config validation with the explicit genesis/config you provide. -- If the proposed config change would alter the genesis hash, treat it as a - different chain and use a fresh data directory instead of trying to mutate - the existing one in place. -- If a new fork point is no longer entirely in the future, compatibility checks - may return a `ConfigCompatError` and require a rewind or another migration - workflow instead of an in-place update. +- Running `XDC init` on a non-empty data directory does not recreate the canonical genesis block when the genesis hash is unchanged. It re-runs startup chain-config validation with the explicit genesis/config you provide. +- If the proposed config change would alter the genesis hash, treat it as a different chain and use a fresh data directory instead of trying to mutate the existing one in place. +- If a new fork point is no longer entirely in the future, compatibility checks may return a `ConfigCompatError` and require a rewind or another migration workflow instead of an in-place update. ## Prague / EIP-2935 - The history storage contract is predeployed only in the developer genesis. -- For other networks, the contract is deployed at Prague activation by the - system call during block processing and state access. -- Nodes upgrading after Prague will perform a one-time backfill of recent - parent hashes into the ring buffer. If historical headers are pruned or - unavailable, missing slots are skipped and only available hashes are filled. -- If you maintain a custom genesis and want predeployment, add an account entry - for `HistoryStorageAddress` with `Nonce: 1` and `Code: HistoryStorageCode`. +- For other networks, the contract is deployed at Prague activation by the system call during block processing and state access. +- Nodes upgrading after Prague will perform a one-time backfill of recent parent hashes into the ring buffer. If historical headers are pruned or unavailable, missing slots are skipped and only available hashes are filled. +- If you maintain a custom genesis and want predeployment, add an account entry for `HistoryStorageAddress` with `Nonce: 1` and `Code: HistoryStorageCode`. From 9d7c8786ab4b09345426d205ea5add71aa31eade Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 08:07:59 +0800 Subject: [PATCH 2/9] docs(solidity): rewrite the gas price notes against block.basefee The minimum gas price the pool accepts is the gas price of the block's tier, the same value `block.basefee` reports, and the two were documented a section apart: the special variable under `## Special variables`, the pool floor in a top-level `## Gas price` section of the same file. Say it once, in the section that already explains what the tier is. The floor sentence, the per-network table and the `eth_gasPrice` advice move under `### block.basefee`, and the standalone section goes away. The sentence also records that transactions to the block-signer and randomize system contracts are exempt from the floor, and the file loses the blank line the removed section had left at the end. The two paragraphs this commit edits in place, the pool-acceptance sentence and the `blobbasefee` note, also move to one line per paragraph. The rest of the rewrap stays with the commit that documents the Amsterdam evm version. --- docs/solidity.md | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/docs/solidity.md b/docs/solidity.md index 3435326907d5..f2f402c429cb 100644 --- a/docs/solidity.md +++ b/docs/solidity.md @@ -64,7 +64,19 @@ The value of `block.prevrandao` is `keccak256(block.number)` in our current impl ### block.basefee -The value of `block.basefee` is the gas price of the block's tier: `12.5 GWei` up to the Gas2500x fork, and `625 GWei` from it. Between the London and EIP-1559 forks, where block headers carry no base fee, it is `12.5 GWei`. +The value of `block.basefee` is the gas price of the block's tier: `12.5 GWei` +up to the Gas2500x fork, and `625 GWei` from it. Between the London and +EIP-1559 forks, where block headers carry no base fee, it is `12.5 GWei`. + +The pool accepts a transaction only when its gas price reaches that same value, the base fee of the block that would include it, and rejects anything priced below with `under min gas price`; transactions to the block-signer and randomize system contracts are exempt. Tools that default to a low `gasPrice` or `maxFeePerGas`, Hardhat and Foundry among them, are rejected until it is raised: + +| Network | Gas tier fork | Base fee and minimum gas price | +| :------ | :----------------- | -----------------------------: | +| mainnet | Gas50x (80370000) | 12.5 GWei | +| testnet | Gas50x (56828700) | 12.5 GWei | +| devnet | Gas2500x (1385100) | 625 GWei | + +Read the floor from the node with `eth_gasPrice` instead of hardcoding it. ### blobhash() @@ -82,22 +94,4 @@ empty output. ### block.blobbasefee -The value of `block.blobbasefee` is always 0 in our EIP-7516 implemention. -EIP-7516 sets a floor of 1 wei; XDPoSChain stays at 0 because it runs no blob fee -market. Do not use it to infer on-chain activity. - -## Gas price - -A transaction priced below the floor of the tier active at the block that would -include it is rejected with `under min gas price`. That floor is the gas price of -the tier, the same value `block.basefee` reports there, so tools that default to -a low `gasPrice` or `maxFeePerGas` (Hardhat and Foundry among them) must have it -raised explicitly: - -| Network | Gas tier fork | Minimum gas price | -| :------ | :----------------- | ----------------: | -| mainnet | Gas50x (80370000) | 12.5 GWei | -| testnet | Gas50x (56828700) | 12.5 GWei | -| devnet | Gas2500x (1385100) | 625 GWei | - -Read the value from the node with `eth_gasPrice` instead of hardcoding it. +The value of `block.blobbasefee` is always 0 in our EIP-7516 implemention. EIP-7516 sets a floor of 1 wei; XDPoSChain stays at 0 because it runs no blob fee market. Do not use it to infer on-chain activity. From ba651d6540b22740de5d4354e0a60227f3d60089 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 08:08:09 +0800 Subject: [PATCH 3/9] docs(solidity): rewrap the notes and document the amsterdam evm version Amsterdam is a fork this chain now has a switch for and a target the compiler started exposing in 0.8.36, but the compatibility table stops at Osaka and never mentions it, so a reader cannot tell whether `--evm-version amsterdam` artifacts work here. They do not: the fork is not implemented (EIP-7843, EIP-8037 and EIP-8038 are missing) and no network has scheduled it. Add an Amsterdam column that is TBD on every network, and a note recording that 0.8.36 began exposing the target and 0.8.37 added `block.slotnum` and `slotnum()`, both only with `--evm-version amsterdam`, still experimental and not the compiler default. The code and initcode limits EIP-7954 raises belong to Amsterdam rather than Osaka, so the size-limit section states them too. The note says the switch exists while the instruction set does not cover the target, because at that point the evm version is reachable in the table but not in the compiler. The file also moves to one line per paragraph, matching the format rule the rest of the change follows; no text changes with it. --- docs/solidity.md | 64 ++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/docs/solidity.md b/docs/solidity.md index f2f402c429cb..6654e29488a5 100644 --- a/docs/solidity.md +++ b/docs/solidity.md @@ -2,16 +2,15 @@ ## Compatibility -XDPoSChain compatibility is determined by the target EVM version, not by a -single Solidity compiler version. +XDPoSChain compatibility is determined by the target EVM version, not by a single Solidity compiler version. Current network activation block numbers: -| Network | Cancun | Prague | Osaka | -| :-----: | -------: | :----: | :---: | -| mainnet | 98802000 | TBD | TBD | -| testnet | 71551800 | TBD | TBD | -| devnet | 25000 | 50000 | TBD | +| Network | Cancun | Prague | Osaka | Amsterdam | +| :-----: | -------: | :----: | :---: | :-------: | +| mainnet | 98802000 | TBD | TBD | TBD | +| testnet | 71551800 | TBD | TBD | TBD | +| devnet | 25000 | 50000 | TBD | TBD | Recommended compiler settings: @@ -23,37 +22,24 @@ Recommended compiler settings: Notes: -1. `evmVersion` mainly affects opcode availability and code generation strategy, - not Solidity language syntax itself. -2. (0.8.30) pin `evmVersion: "cancun"` only if you need reproducible outputs - with existing Cancun-targeted artifacts. -3. (0.8.31+) set `evmVersion` to `cancun` (or `prague` after Prague activation) - unless your target network has Osaka. -4. The repository contains fixtures generated by `solc 0.8.28`, but that is not - a hard upper bound for supported Solidity versions. -5. If Prague is activated on XDPoSChain in the future, Solidity 0.8.30 and later - can target it by setting `evmVersion: "prague"` explicitly. -6. Solidity 0.8.31 introduced support for the `CLZ` opcode (EIP-7939), which is - part of Osaka, not Prague. The strong pinning warning applies to Osaka-only - features (such as CLZ), not to 0.8.30's default `prague` target by itself. +1. `evmVersion` mainly affects opcode availability and code generation strategy, not Solidity language syntax itself. +2. (0.8.30) pin `evmVersion: "cancun"` only if you need reproducible outputs with existing Cancun-targeted artifacts. +3. (0.8.31+) set `evmVersion` to `cancun` (or `prague` after Prague activation) unless your target network has Osaka. Do not set it to `amsterdam`; see note 7. +4. The repository contains fixtures generated by `solc 0.8.28`, but that is not a hard upper bound for supported Solidity versions. +5. If Prague is activated on XDPoSChain in the future, Solidity 0.8.30 and later can target it by setting `evmVersion: "prague"` explicitly. +6. Solidity 0.8.31 introduced support for the `CLZ` opcode (EIP-7939), which is part of Osaka, not Prague. The strong pinning warning applies to Osaka-only features (such as CLZ), not to 0.8.30's default `prague` target by itself. +7. Solidity 0.8.36 began exposing the `amsterdam` target and 0.8.37 added `block.slotnum` and `slotnum()` to it (EIP-7843), both only with `--evm-version amsterdam`; it is still experimental and is not the compiler's default target. XDPoSChain cannot serve it: the fork switch exists, but its instruction set is Osaka plus EIP-8024 only, because EIP-7843, EIP-8037 and EIP-8038 are not implemented here, and no network in the table above has scheduled Amsterdam. Keep the `osaka` target, or pin `evmVersion` to an older one, until those EIPs land. ## Transaction and block size limits -XDPoSChain enforces neither of the two caps Osaka introduces on Ethereum: -EIP-7825 limits a transaction to 16,777,216 gas and EIP-7934 limits an -RLP-encoded block to 8 MiB. Both are implemented here and both are gated -behind Osaka, which no network has scheduled yet, so neither applies today. -For reference, the public networks run with a block gas limit of 420,000,000, -set through the node's `--targetgaslimit`; the in-repository default -(`XDCGenesisGasLimit`) is 42,000,000. +XDPoSChain enforces neither of the two caps Osaka introduces on Ethereum: EIP-7825 limits a transaction to 16,777,216 gas and EIP-7934 limits an RLP-encoded block to 8 MiB. Both are implemented here and both are gated behind Osaka, which no network has scheduled yet, so neither applies today. For reference, the public networks run with a block gas limit of 420,000,000, set through the node's `--targetgaslimit`; the in-repository default (`XDCGenesisGasLimit`) is 42,000,000. -A transaction or deployment that spends more than 16,777,216 gas therefore -succeeds here but cannot be replayed on an Ethereum network that runs those -caps, which makes such artifacts migrate one way only. Stay below the cap if -the contract has to remain portable. +A transaction or deployment that spends more than 16,777,216 gas therefore succeeds here but cannot be replayed on an Ethereum network that runs those caps, which makes such artifacts migrate one way only. Stay below the cap if the contract has to remain portable. Both caps take effect by themselves once Osaka is scheduled and active. +Contract code and initcode are limited separately, by EIP-7954: 24,576 bytes of code and 49,152 of initcode today, rising to 65,536 and 131,072 once Amsterdam is active. That raise is gated on Amsterdam, which is where upstream go-ethereum has it, not on Osaka. No network has scheduled Amsterdam, so the lower pair applies everywhere today. + ## Special variables ### block.prevrandao @@ -64,9 +50,7 @@ The value of `block.prevrandao` is `keccak256(block.number)` in our current impl ### block.basefee -The value of `block.basefee` is the gas price of the block's tier: `12.5 GWei` -up to the Gas2500x fork, and `625 GWei` from it. Between the London and -EIP-1559 forks, where block headers carry no base fee, it is `12.5 GWei`. +The value of `block.basefee` is the gas price of the block's tier: `12.5 GWei` up to the Gas2500x fork, and `625 GWei` from it. Between the London and EIP-1559 forks, where block headers carry no base fee, it is `12.5 GWei`. The pool accepts a transaction only when its gas price reaches that same value, the base fee of the block that would include it, and rejects anything priced below with `under min gas price`; transactions to the block-signer and randomize system contracts are exempt. Tools that default to a low `gasPrice` or `maxFeePerGas`, Hardhat and Foundry among them, are rejected until it is raised: @@ -80,17 +64,7 @@ Read the floor from the node with `eth_gasPrice` instead of hardcoding it. ### blobhash() -XDPoSChain has no blob transactions (transaction type `0x03`) and no blob fields -in the block header, so `blobhash(i)` always returns 0. That is the out-of-bounds -value defined by EIP-4844, so `require(blobhash(i) != 0)` reverts as expected. -The KZG point evaluation precompile (`0x0a`) is not implemented. On the networks -that run the Prague precompile set (devnet from block 50000, localnet from the -genesis block) its address is registered and every call to it fails, consuming -the gas of the frame. No network runs the Osaka set yet, because every network -configuration still leaves `osakaBlock` unset. On mainnet and Apothem, whose -Prague and Osaka blocks are still unset, the address is not registered at all, so -a call to it is handled as a call to an empty account: it succeeds and returns -empty output. +XDPoSChain has no blob transactions (transaction type `0x03`) and no blob fields in the block header, so `blobhash(i)` always returns 0. That is the out-of-bounds value defined by EIP-4844, so `require(blobhash(i) != 0)` reverts as expected. The KZG point evaluation precompile (`0x0a`) is not implemented. On the networks that run the Prague precompile set (devnet from block 50000, localnet from the genesis block) its address is registered and every call to it fails, consuming the gas of the frame. No network runs the Osaka set yet, because every network configuration still leaves `osakaBlock` unset. On mainnet and Apothem, whose Prague and Osaka blocks are still unset, the address is not registered at all, so a call to it is handled as a call to an empty account: it succeeds and returns empty output. ### block.blobbasefee From 47b232ebe368e3557ad77cc099c995f3f5c27e23 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 08:17:17 +0800 Subject: [PATCH 4/9] docs(solidity): document the prague eip-2935 history contract The upgrade notes describe where the EIP-2935 history contract is deployed and how it is backfilled, which is what an operator needs. What a contract can do with it was not written down anywhere: `docs/solidity.md` documents the special variables a contract reads, and the history contract is the way to read a block hash older than the 256 `blockhash` answers for. Add that half. `blockhash` is unchanged, the contract keeps the last 8191 hashes, reading one means calling it with the block number, and a number outside that window reverts rather than returning zero. The deployment and backfill rules stay in `docs/upgrade.md`, which this commit does not touch, so the two files split by audience rather than duplicating each other. --- docs/solidity.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/solidity.md b/docs/solidity.md index 6654e29488a5..ef67065b4491 100644 --- a/docs/solidity.md +++ b/docs/solidity.md @@ -40,6 +40,14 @@ Both caps take effect by themselves once Osaka is scheduled and active. Contract code and initcode are limited separately, by EIP-7954: 24,576 bytes of code and 49,152 of initcode today, rising to 65,536 and 131,072 once Amsterdam is active. That raise is gated on Amsterdam, which is where upstream go-ethereum has it, not on Osaka. No network has scheduled Amsterdam, so the lower pair applies everywhere today. +## Prague / EIP-2935 + +Prague adds a history storage contract at `HistoryStorageAddress`, `0x0000F90827F1C53a10cb7A02335B175320002935`, which holds the hashes of the last 8191 blocks. + +`blockhash` itself is unchanged: it answers only for the last 256 blocks and returns zero beyond that. To read an older hash, call the history contract with the block number; it returns the hash it stored for that block, and reverts for a number outside the window it keeps. A slot inside the window that was never written reads back as zero, so right after activation most older numbers return zero rather than a hash. + +The contract is only present once Prague is active on the network, so there is nothing to call on mainnet or testnet yet. + ## Special variables ### block.prevrandao From bb9bf124b2422a57f662518ddab335bf984f3fc4 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 10:34:38 +0800 Subject: [PATCH 5/9] docs(upgrade): correct the eip-2935 deployment and backfill notes The section described a backfill of recent parent hashes at upgrade time, which neither the code nor EIP-2935 does: block processing deploys the contract at the activation block, writes that block's parent hash into one slot, and every later block adds one more, so the rest of the window reads back as zero until 8,191 blocks have passed. Document that route together with the upstream one, where the account is created by the presigned deployment transaction the EIP specifies and clients carry no deployment code of their own, and record the two further local deviations in the failure handling and the gas budget. --- docs/upgrade.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/upgrade.md b/docs/upgrade.md index 501196a43bf2..9257b7779a3a 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -554,7 +554,11 @@ Use this procedure when the chain is already running and you want to add a new f ## Prague / EIP-2935 -- The history storage contract is predeployed only in the developer genesis. -- For other networks, the contract is deployed at Prague activation by the system call during block processing and state access. -- Nodes upgrading after Prague will perform a one-time backfill of recent parent hashes into the ring buffer. If historical headers are pruned or unavailable, missing slots are skipped and only available hashes are filled. +The history storage contract at `HistoryStorageAddress` records the hashes of the last 8,191 blocks so that contracts can read them; `blockhash` itself still answers for 256 blocks only. What follows is how the contract gets into the state, which is where this chain differs from upstream go-ethereum. + +- EIP-2935 specifies that the account is created by a presigned synthetic deployment transaction, sender `0x3462413Af4609098e1E27A490f554f260213D685`, transaction hash `0x67139a552b0d3fffc30c0fa7d0c20d42144138c8fe07fc5691f09c1cce632e15`. Upstream go-ethereum carries no deployment code of its own: the account exists once that transaction is included in a block, and the bundled mainnet and sepolia genesis allocations do not contain it. +- XDPoSChain does not use that transaction. Block processing deploys the contract itself at the Prague activation block, only when the code is missing, as a state change just before the system call that stores the parent hash. The account ends up the same either way, `Nonce: 1` with `Code: HistoryStorageCode` and exempt from EIP-161 cleanup; the developer genesis predeploys it, so that step is a no-op there. +- The ring buffer starts with one entry, the parent hash of the activation block, and every following block adds one more. The window takes 8,191 blocks after activation to fill, and any slot not yet written reads back as zero. EIP-2935 states the same under its activation scenarios and requires every other slot to be `0`. +- A one-time catch-up runs in the same step whenever the code is missing at a block at or after activation, for example after a state restore. It fills what it can still resolve out of the last 8,191 blocks and skips the slots whose headers are pruned or unavailable. This has no counterpart upstream, where EIP-2935 never has the contract absent after activation. Upgrading to this binary does not trigger a backfill of its own, and a node that already has the code never enters the branch. +- Two further local deviations, neither reachable today: a failed system call, or code at the address that differs from `HistoryStorageCode`, panics instead of being ignored as upstream does, and the call keeps the 30,000,000 gas limit EIP-2935 prescribes, with no counterpart to the state budget upstream adds to system calls from Amsterdam on. - If you maintain a custom genesis and want predeployment, add an account entry for `HistoryStorageAddress` with `Nonce: 1` and `Code: HistoryStorageCode`. From 815962fcee5f055ad14d51e9d7bcfe34f9805127 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 10:34:49 +0800 Subject: [PATCH 6/9] docs(solidity): drop the stale prague premise from note 5 The note still asked the reader to imagine Prague activating "in the future", while the activation table in the same file has carried devnet at block 50000 and the Prague section added alongside it describes what is already scheduled. --- docs/solidity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/solidity.md b/docs/solidity.md index ef67065b4491..764aeb37d792 100644 --- a/docs/solidity.md +++ b/docs/solidity.md @@ -26,7 +26,7 @@ Notes: 2. (0.8.30) pin `evmVersion: "cancun"` only if you need reproducible outputs with existing Cancun-targeted artifacts. 3. (0.8.31+) set `evmVersion` to `cancun` (or `prague` after Prague activation) unless your target network has Osaka. Do not set it to `amsterdam`; see note 7. 4. The repository contains fixtures generated by `solc 0.8.28`, but that is not a hard upper bound for supported Solidity versions. -5. If Prague is activated on XDPoSChain in the future, Solidity 0.8.30 and later can target it by setting `evmVersion: "prague"` explicitly. +5. Prague is scheduled on devnet only; mainnet and testnet still show TBD above. Once it activates on a network you target, Solidity 0.8.30 and later can target it by setting `evmVersion: "prague"` explicitly. 6. Solidity 0.8.31 introduced support for the `CLZ` opcode (EIP-7939), which is part of Osaka, not Prague. The strong pinning warning applies to Osaka-only features (such as CLZ), not to 0.8.30's default `prague` target by itself. 7. Solidity 0.8.36 began exposing the `amsterdam` target and 0.8.37 added `block.slotnum` and `slotnum()` to it (EIP-7843), both only with `--evm-version amsterdam`; it is still experimental and is not the compiler's default target. XDPoSChain cannot serve it: the fork switch exists, but its instruction set is Osaka plus EIP-8024 only, because EIP-7843, EIP-8037 and EIP-8038 are not implemented here, and no network in the table above has scheduled Amsterdam. Keep the `osaka` target, or pin `evmVersion` to an older one, until those EIPs land. From 16eafb4adb58983f3626ddbb49850eb45d0697d2 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 10:34:49 +0800 Subject: [PATCH 7/9] docs(solidity): add the testnet gas2500x tier to the base fee table The table named one tier fork per network, but testnet also has Gas2500xBlock set at 83600000, where the floor moves from 12.5 GWei to 625 GWei. The prose already described that transition; the table an operator looks up did not. --- docs/solidity.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/solidity.md b/docs/solidity.md index 764aeb37d792..7246e72dcdfb 100644 --- a/docs/solidity.md +++ b/docs/solidity.md @@ -62,11 +62,12 @@ The value of `block.basefee` is the gas price of the block's tier: `12.5 GWei` u The pool accepts a transaction only when its gas price reaches that same value, the base fee of the block that would include it, and rejects anything priced below with `under min gas price`; transactions to the block-signer and randomize system contracts are exempt. Tools that default to a low `gasPrice` or `maxFeePerGas`, Hardhat and Foundry among them, are rejected until it is raised: -| Network | Gas tier fork | Base fee and minimum gas price | -| :------ | :----------------- | -----------------------------: | -| mainnet | Gas50x (80370000) | 12.5 GWei | -| testnet | Gas50x (56828700) | 12.5 GWei | -| devnet | Gas2500x (1385100) | 625 GWei | +| Network | Gas tier fork | Base fee and minimum gas price | +| :------ | :------------------ | -------------------------------: | +| mainnet | Gas50x (80370000) | 12.5 GWei | +| testnet | Gas50x (56828700) | 12.5 GWei | +| testnet | Gas2500x (83600000) | 625 GWei | +| devnet | Gas2500x (1385100) | 625 GWei | Read the floor from the node with `eth_gasPrice` instead of hardcoding it. From d52db7af8af849af9634786d3817463632d72d9f Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 10:34:49 +0800 Subject: [PATCH 8/9] docs(upgrade): generalise the fork declaration requirement The section named Prague alone, but amsterdamBlock joins the same rule: it is part of the built-in backfill set and is not inherited by custom-network hydration, so it needs the same explicit declaration. Naming the latest two keeps the rule readable as forks are added. --- docs/upgrade.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/upgrade.md b/docs/upgrade.md index 9257b7779a3a..c21525f8f377 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -163,11 +163,11 @@ Operational note for custom/private networks: - The node now emits a `WARN` listing the auto-filled fields when this custom-Localnet fallback happens through `BackfillMissingFieldsFrom`. - To disable this behavior, declare every fork field explicitly in your custom chain config instead of relying on omission. Use `null` to disable a pointer-based fork, and `0` or another block number only when that activation is intentional. -Prague declaration requirement: +Fork declaration requirement: -- Every chain config should now declare `pragueBlock` explicitly. -- Use `"pragueBlock": null` to keep Prague disabled, or a positive integer to schedule activation. -- Do not rely on omission as a long-term configuration strategy. Legacy built-in and Localnet configs can still have a missing `pragueBlock` hydrated from their bundled backfill source through `BackfillMissingFieldsFrom`, but custom-network hydration does not inherit later-added fork switches automatically. +- Every chain config should now declare each fork switch explicitly, starting with the two latest, `pragueBlock` and `amsterdamBlock`. +- Use `null` to keep a fork disabled, or a positive integer to schedule activation. +- Do not rely on omission as a long-term configuration strategy. Legacy built-in and Localnet configs can still have a missing `pragueBlock` hydrated from their bundled backfill source through `BackfillMissingFieldsFrom`, but custom-network hydration does not inherit later-added fork switches automatically, and `amsterdamBlock` sits in the same built-in backfill set as `pragueBlock`. ## When a New ChainConfig Is Written to the Database From a63d6d0b64454fd0626acee9e98bc93ea86b8013 Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Fri, 9 Oct 2026 12:16:09 +0800 Subject: [PATCH 9/9] docs(solidity,upgrade): fix pre-existing spelling and quotation slips Both slips predate this branch. `implemention` in the `block.prevrandao` and `block.blobbasefee` notes is present at the base commit (lines 61 and 85 there), and the "semantically different" sentence in the upgrade notes already used curly quotes (line 203 there). The gas-price and rewrap commits in this branch reproduced both verbatim, so they arrive unchanged. Fix the spelling and match the straight quotes used everywhere else in both files; no other wording changes. --- docs/solidity.md | 4 ++-- docs/upgrade.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/solidity.md b/docs/solidity.md index 7246e72dcdfb..16ffde2927d4 100644 --- a/docs/solidity.md +++ b/docs/solidity.md @@ -52,7 +52,7 @@ The contract is only present once Prague is active on the network, so there is n ### block.prevrandao -The value of `block.prevrandao` is `keccak256(block.number)` in our current implemention. It is predictable and unsafe. +The value of `block.prevrandao` is `keccak256(block.number)` in our current implementation. It is predictable and unsafe. **NOTICE: do not use it in real business.** @@ -77,4 +77,4 @@ XDPoSChain has no blob transactions (transaction type `0x03`) and no blob fields ### block.blobbasefee -The value of `block.blobbasefee` is always 0 in our EIP-7516 implemention. EIP-7516 sets a floor of 1 wei; XDPoSChain stays at 0 because it runs no blob fee market. Do not use it to infer on-chain activity. +The value of `block.blobbasefee` is always 0 in our EIP-7516 implementation. EIP-7516 sets a floor of 1 wei; XDPoSChain stays at 0 because it runs no blob fee market. Do not use it to infer on-chain activity. diff --git a/docs/upgrade.md b/docs/upgrade.md index c21525f8f377..b46ce041d00c 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -178,7 +178,7 @@ The node does not rewrite the database on every startup. 3. If the two effective configs are semantically identical, startup does not rewrite the stored chain-config blob just because runtime backfill filled omitted fields in memory. 4. If the new effective config is semantically different and compatibility checks pass, the node writes the new resolved `ChainConfig` to the database. -In this context, “semantically different” means the two configs differ after ignoring JSON field-presence tracking. In other words, field omission alone is not treated as a meaningful change if the resolved values are the same. +In this context, "semantically different" means the two configs differ after ignoring JSON field-presence tracking. In other words, field omission alone is not treated as a meaningful change if the resolved values are the same. Additional notes: