Repository navigation
bsc: apply Parlia system-contract upgrades at fork boundaries - #24029
sudeepdino008 wants to merge 21 commits into
Conversation
In StagesOnlyBlocks mode the Execution stage is a no-op and never advances its progress, so Finish skipped writing HeadBlockHash (it gated on the Execution frontier) and the block head stayed at genesis. Advance the head to the Senders frontier in that mode instead. Lets a blocks-only pipeline commit + report a real head (used by the BSC block-download phase before execution exists).
Sync.TargetBlock stops the block-download driver once the head reaches it (0 = no bound).
New bsc/sync driver: an execution-module client (Astrid/driver-DB model) that fetches headers+bodies over devp2p and persists them via execModule InsertBlocks + UpdateForkChoice -- no direct rawdb, no execution, no seal verification; reorg/canonical handled inside UpdateForkChoice. Verify is structural only (SanityCheck + HashCheck). Resumes from max(FrozenBlocks, CurrentHeader) on restart; UpdateForkChoice every LoopBlockLimit blocks. Wired into backend's Parlia dispatch arm (replacing the block printer), which enables StagesOnlyBlocks for Parlia until execution lands. Verified on Chapel: forward download + resume, retire to .seg + prune, seg integrity publishable.
Generalized fork of bor's deleted polygon/sync block downloader (downloadBlocksUsingWaypoints): waypoints → block-number ranges (default 1024, ~1GB/worker at 1MB/block matching bor; RAM-bounded worker count scales with the range size), generated lazily from a cursor so a full-chain sync doesn't pre-allocate a ~123k-entry range list. Worker pool min(maxWorkers, peers, ranges) fetches ranges concurrently across peers; per-range verifyChain (bor's blocksVerifier plus the inter-range boundary linkage waypoints gave for free); gap truncation rewinds the cursor to the failed range; peer-poll backoff. Ordered batches go to the same InsertBlocks+UpdateForkChoice seam. Used for bounded bulk (target>0); the single-peer sequential loop stays for the tip-less fallback. Verified on Chapel: multi-peer parallel fetch (workers up to 4), resume-on-restart, no BadBlock.
|
Reviewed the upgrade commit only, not the branch it is stacked on. One bug, one design point. A missing parent header applies every past timestamp upgrade at once, in random order.
|
54ff174 to
2c68743
Compare
… fee model) Turns on real execution for BSC/Parlia (was download-only). Blocks 1-89 execute with state roots matching the live Chapel chain; the next divergence (block 90) needs the BSC light-client precompiles, which are not part of this change. Two BSC-specific rules are required for a correct state root: - System transactions (the validator's gas-price-zero calls to system contracts) run as free consensus calls, outside the block gas pool: bsc/parlia adds the IsSystemTransaction predicate + system-contract set, and execution/exec routes them to executeSystemTx, which bumps the validator nonce and mirrors distributeToSystem/distributeToValidator's direct SystemAddress->validator move before forwarding the value on-chain. - User-tx fees are credited to SystemAddress, not the coinbase (execution/protocol); Finalize later splits them to the SystemReward/Validator contracts. execution/stagedsync keeps Parlia on the serial executor for now, since system-tx handling relies on serial ordering.
The executor no longer knows BSC consensus rules. It recognizes a system tx via the new rules.SystemTxEngine interface and provides a run closure (nonce bump + free EVM call); Parlia.ApplySystemTx owns the state effect (the SystemAddress -> validator reward move) and invokes run. No churn on the other engines - the interface is optional and type-asserted.
Parlia embeds system transactions in the block body and routes the tx tip to SystemAddress (which those system txs then read). Make the parallel executor handle both: - tag body system txs (chain.Rules.IsParlia + engine.IsSystemTransaction) and skip the block-gas-budget inclusion check for them (their gas limit is a sentinel). - apply fees inline for Parlia (no fee delaying) and skip the deferred coinbase fee credit, so the in-block system transactions read the up-to-date SystemAddress balance through the version map. Serial and parallel both execute Chapel with matching state roots.
…eipt bugs - drop Rules.IsParlia; the deferred-fee skip keys on the task's shouldDelayFeeCalc, which already means "fees not delayed". - drop asSystemTxEngine and SystemTxRun: executeSystemTx takes rules.Engine and does the nonce bump + call itself; the engine's ApplySystemTx only performs the SystemAddress -> validator reward move. - a reverted system tx is now an invalid block (matches BSC), not a failed receipt. - credit the blob fee to SystemAddress after Cancun (was burnt), matching BSC. - prepare the access list for system-tx calls after Cancun so warm/cold gas matches.
…avlMerkleProofValidate)
Ports BSC's Tendermint light-client + IAVL merkle-proof precompiles so Chapel
gets past block 90 (its LightClient/CrossChain txs invoke them). Execution now
runs from genesis to ~block 12k before the next (unrelated) divergence.
- vendor core/vm/lightclient/{v1,iavl} from bsc-erigon verbatim (import paths
rewritten only); pull the BNB tendermint fork + go-amino + ics23; exclude the
vendored surface from lint/gofmt.
- register PrecompiledContractsIstanbulForBSC (standard Istanbul + 0x64/0x65),
gated on chain.Rules.IsParlia (re-added; here it's the canonical precompile
gate). Later BSC-fork precompile sets are follow-ups.
- executeSystemTx: convert a block-STM ErrDependency panic into a retriable
abort (as the metered path does), so system txs compose with parallel exec
past the first heavily-contended block.
…3 dep Only the Istanbul-era precompiles (0x64 tmHeaderValidate, 0x65 iavlMerkleProofValidate) are registered. Delete the unregistered Nano/Moran/Planck/Plato variants, their op-verifiers, and the ics23 proof op; each should be re-added in the PR that enables its fork, setting verifier fields at build time (writing them in Run races across parallel workers sharing one instance). Inline basicIavlMerkleProofValidate into iavlMerkleProofValidate.Run with a local DefaultProofRuntime so the live precompile has no mutable receiver state. Drop github.com/bnb-chain/ics23 (the tendermint fork does not require it) and the file-level golangci exclusion for contracts_lightclient.go.
Parlia routes the user-tx tip (and post-Cancun blob fee) to SystemAddress, which the in-block system tx reads every block. Crediting it inline in the parallel workers (the prior approach) made every user tx read-modify-write SystemAddress and raced the system tx's read, so parallel exec diverged non-deterministically (wrong trie root at 4243/4629/12191/...). Defer the Parlia tip through calcFees like the coinbase: shouldDelayFeeCalc is now always true, recipient+amount are threaded as calcFees params, and the blob fee is carried in ExecutionResult.FeeBlob. Workers no longer touch SystemAddress, so tip credits become fresh version cells and the in-block system tx's read of SystemAddress re-executes normally under block-STM. Ethereum path unchanged: the recipient/amount are the coinbase/tip verbatim; the Parlia branch only swaps in SystemAddress and adds the blob fee. Verified: 17/17 clean fresh parallel runs to blocks 20000/50000 (was non-deterministic before). Residual same-version-restamp gap under many fee-crediting txs per block is documented inline for high-contention BSC.
…derive recipient in calcFees
At each BSC fork the system contracts' bytecode is replaced. The upgrade code is carried in the chain config as parlia.blockAlloc (keyed by block number or unix timestamp -> genesis-alloc); Parlia.Initialize applies it via SetCode at block initialisation so the block's transactions observe the new code. Running it in the engine's Initialize hook covers serial, parallel and historical-trace paths from one site. Without this, execution diverged at the Ramanujan fork block (Chapel 1,010,000): the system contracts kept their genesis code, giving a wrong state root. Verified Chapel now executes through Ramanujan and the 1,014,369 upgrade set. Ported from bsc-erigon (core/systemcontracts). The SystemContractCodeLookup historical-code workaround is not needed on erigon3.
…nt time Parse BlockAlloc once at engine construction into a sorted, typed slice instead of re-parsing keys and re-decoding bytecode on every block; a malformed key or alloc now fails fast at startup rather than mid-sync. Sorting makes overlapping addresses across forks resolve deterministically (later fork wins). A timestamp-based fork needs the parent block time to detect the crossing block; treat a missing parent header as a hard error rather than a silent zero, which would otherwise apply every past timestamp upgrade at once in map-iteration order (a non-deterministic wrong root). Keys are classified block-number vs timestamp by a documented boundary so a timestamp key can never match by block number.
4d12324 to
377660b
Compare
domiwei
left a comment
There was a problem hiding this comment.
The post-Feynman upgrade sets are applied at the wrong point in the block, and the Chapel Pasteur set is missing. Both are consensus deviations from upstream BSC and need a fix before merge; see the inline comments.
What matches upstream:
- The 19 upgrade sets match bsc-erigon's
blockAlloc, and their contract code matches BSC geth. - Block keys activate on the exact block, and time keys on the crossing parent.Time < T <= header.Time.
- Ramanujan through Plato, Kepler and Feynman are applied at block begin, as geth does.
Also inline: a nil-reader panic on an RPC path, and a stale note in bsc/PROGRESS.md.
Non-blocking, but please track it: the Prague EIP-2935 setup is missing for Parlia. Upstream sets the HistoryStorageAddress code and nonce 1 at the Prague block (geth core/systemcontracts/upgrade.go:1116-1121), and from that block on it writes the parent hash on every block (core/state_processor.go:113). This is outside the blockAlloc mechanism, but on Chapel it diverges at time 1740452880 (block 48,576,786), the same block as the Pascal set.
Non-blocking, but important: bsc/parlia has no tests for parseSystemContractUpgrades / upgradeSystemContracts. The only new test pins the key list and a digest of the data. Please add:
- a table test for block keys (block before, at and after), time keys (parent before, at and after T), one block that crosses two time keys, and the missing-parent error;
- an execution-level test for the post-Feynman application point relative to user and system txs.
The failing race-tests job (p2p/dnsdisc TestIteratorLinkUpdates) is in code that this diff does not touch.
| func (p *Parlia) Initialize(config *chain.Config, chain rules.ChainHeaderReader, header *types.Header, | ||
| state *state.IntraBlockState, syscall rules.SysCallCustom, logger log.Logger, tracer *tracing.Hooks) error { | ||
| ibs *state.IntraBlockState, syscall rules.SysCallCustom, logger log.Logger, tracer *tracing.Hooks) error { | ||
| return p.upgradeSystemContracts(chain, header, ibs) |
There was a problem hiding this comment.
Blocker: post-Feynman upgrade sets are applied before the user transactions. Upstream applies them after the user transactions and before the system transactions.
BSC geth applies upgrades at block begin only while the parent is pre-Feynman: TryUpdateBuildInSystemContract(..., atBlockBegin=true) checks !config.IsFeynman(blockNumber, lastBlockTime) (core/systemcontracts/upgrade.go:1111-1126). Otherwise the upgrade runs from Parlia.Finalize (consensus/parlia/parlia.go:1423), after all user txs and before the system txs. bsc-erigon applies the later sets the same way, before the first system tx (execution/consensus/parlia/parlia.go:1023-1026, execution/exec3/state.go:250). It checks the block's own time instead of the parent time, so it differs from geth only at the Feynman block itself.
Affected Chapel keys and their fork blocks:
- 1711342800: block 38,876,951
- 1719986788: block 41,756,117
- 1724116996: block 43,132,201
- 1740452880: block 48,576,786
- 1744097580: block 49,791,365
- 1748243100: block 52,552,978
- 1762741500: block 71,859,053
Failing scenario: a user tx in one of these blocks calls, delegatecalls, or reads the code (EXTCODEHASH/EXTCODESIZE) of an upgraded contract, for example StakeHub 0x2002. Here it runs the new code; upstream runs the old code. Gas, receipts and the state root can then differ. The Feynman block itself and all block-keyed sets are correct, because their parent is pre-Feynman.
Context: I checked these seven Chapel blocks over public RPC. No user tx in them targets an upgraded address directly, and none emits logs from one. I could not check internal calls without trace access. So the existing Chapel history may not diverge, but the rule is still different from upstream, and any block with such a tx will diverge.
Suggested fix: keep block-begin application for sets whose parent is pre-Feynman. Apply the later sets after the last user tx and before the first system tx, or at block end when the block has no system tx. Finalize alone is too late, because here the system txs run as ordinary body txs.
| assert.Equal(t, []string{ | ||
| "1010000", "1014369", "5582500", "13837000", "19203503", "22800220", "23603940", | ||
| "28196022", "29295050", "29861024", "1702972800", "1710136800", "1711342800", | ||
| "1719986788", "1724116996", "1740452880", "1744097580", "1748243100", "1762741500", |
There was a problem hiding this comment.
Blocker: the Chapel Pasteur set is missing.
chapel.json sets pasteurTime: 1784601000 (2026-07-21, already active). At that fork BSC geth replaces the code of StakeHub 0x2002 and Governor 0x2004:
pasteurUpgrade[chapelNet]in core/systemcontracts/upgrade.go:1078-1092;- applied at :1228-1229;
- code in core/systemcontracts/pasteur/chapel/.
This data is ported from bsc-erigon's blockAlloc, which predates Pasteur.
Failing scenario: at the first block with parent.Time < 1784601000 <= header.Time, upstream changes both code hashes and this branch does not. The state root diverges at that block, whatever transactions it contains.
Suggested fix: add bsc/chain/upgrades/chapel/1784601000.json with the two contracts from geth, and extend this key list. The parent of that block is post-Feynman, so it also falls under the timing point in bsc/parlia/parlia.go.
| return nil | ||
| } | ||
| if number > 0 { | ||
| parent := chain.GetHeader(header.ParentHash, number-1) |
There was a problem hiding this comment.
From the first time key (1702972800) on, every call reaches this line, not only calls at fork boundaries.
Stateless witness verification passes a nil reader: execBlockStatelessly at rpc/jsonrpc/debug_execution_witness.go:2023. For a post-Kepler block that reaches this verification, the line calls a method on a nil interface and panics. The RPC server recovers the panic (rpc/service.go:229), so debug_executionWitness and eth_getWitness fail with "method handler crashed". Before this PR, Initialize did not use the reader.
Suggested fix: return an error instead of panicking when chain is nil. Then let the witness path supply the parent: result.headerByNumber already includes the parent header. Another option is to take the parent time from the caller, as bsc-erigon does with lastBlockTime.
A related point: eth_simulateV1 passes a DB-only reader (rpc/jsonrpc/eth_simulation.go:578), so from the second simulated block on it would hit "missing parent header". This is not reachable today, because Parlia's FinalizeAndAssemble still returns errNotSupported.
| | 35,682,300 | HertzFix | | | ||
|
|
||
| Timestamp-activated forks (Shanghai/Kepler/Feynman/Cancun/…, unix ts ≥ ~1.7e9 ≈ block 30M+) | ||
| follow. All 19 `blockAlloc` code-upgrade sets (block- and time-keyed) are present in `chapel.json` |
There was a problem hiding this comment.
nit: this paragraph is out of date:
- The sets now live in
bsc/chain/upgrades/chapel/, not inchapel.json, which has"parlia": {}. - "applied by bsc: apply Parlia system-contract upgrades at fork boundaries #24029" is not accurate yet: Pasteur is missing, and the post-Feynman sets run at block begin.
It would also help to list the missing Prague EIP-2935 setup here as a known gap.
| func (p *Parlia) Initialize(config *chain.Config, chain rules.ChainHeaderReader, header *types.Header, | ||
| state *state.IntraBlockState, syscall rules.SysCallCustom, logger log.Logger, tracer *tracing.Hooks) error { | ||
| ibs *state.IntraBlockState, syscall rules.SysCallCustom, logger log.Logger, tracer *tracing.Hooks) error { | ||
| return p.upgradeSystemContracts(chain, header, ibs) |
There was a problem hiding this comment.
Post-Feynman, upstream applies these upgrades in Finalize after user txs; here a user tx on a fork block runs the new code.
| return nil | ||
| } | ||
| if number > 0 { | ||
| parent := chain.GetHeader(header.ParentHash, number-1) |
There was a problem hiding this comment.
debug_executionWitness passes a nil chain reader and eth_simulateV1 can't resolve simulated parents, so both fail here.
| assert.Equal(t, []string{ | ||
| "1010000", "1014369", "5582500", "13837000", "19203503", "22800220", "23603940", | ||
| "28196022", "29295050", "29861024", "1702972800", "1710136800", "1711342800", | ||
| "1719986788", "1724116996", "1740452880", "1744097580", "1748243100", "1762741500", |
There was a problem hiding this comment.
Chapel Pasteur (StakeHub, Governor at 1784601000) has no upgrade set, and this key list locks the gap in.
|
var parent *types.Header
...
if u.byTime {
if header.Time < u.numOrTime {
continue
}
if parent == nil {
if parent = chain.GetHeader(header.ParentHash, number-1); parent == nil {
return fmt.Errorf("parlia: missing parent header for block %d, cannot evaluate timestamp system-contract upgrade", number)
}
}
if parent.Time >= u.numOrTime {
continue
}
}net: -13 lines possible. |
At some forks, BSC replaces the bytecode of its system contracts (
0x1000,0x1001, …). Without that, the state root diverges from the first fork block (e.g. Ramanujan, Chapel block 1,010,000).ParliaConfig.BlockAlloc: upgrade bytecode keyed by block number or timestamp, ported from bsc-erigoncore/systemcontracts.bsc/chain/upgrades/chapel/<block-or-time>.json: the 19 Chapel upgrade sets, one file per fork, embedded and loaded intoBlockAlloc.Parlia.Initializeapplies the matching set viaSetCodebefore the block's txs, so the serial, parallel and trace paths all see the new code.