chore: remove triedb (rust-eth-triedb/pathdb) state backend - #202
chore: remove triedb (rust-eth-triedb/pathdb) state backend#202chee-chyuan wants to merge 3 commits into
Conversation
Removes all triedb-related code paths, keeping only the MDBX-based state backend. This eliminates the conditional `is_triedb_active()` branching throughout the engine, execution, storage, RPC, and node builder layers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull Request ReviewThis PR removes the TrieDB/pathdb backend entirely and standardizes the codebase on MDBX-only state handling. It deletes triedb-specific dependencies, config/CLI flags, startup alignment logic, trie prefetcher code paths, and conditional branching ( Sensitive ContentNo sensitive content detected. Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Pull Request ReviewThis PR removes all Sensitive ContentNo sensitive content detected. Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
Drops all triedb code paths and the four rust-eth-triedb* git deps so the downstream reth-bsc crate compiles against the upstream Reth tree where PR bnb-chain/reth#202 has removed the same backend. After this change the only state backend is MDBX. Pinned `bnb-chain/reth` to rev 6dcb09c0090e046eea6440fbeb58cce5db0476b2 (PR 202 head) and aligned the bsc-ef-tests crate to the same rev. - evm/builder.rs: collapsed `is_triedb_active()` + finish_with_difflayer override into `fn finish`, keeping the sparse-trie precomputed-root fast path and the MDBX `state_root_with_updates` fallback. Removed the `request_difflayer` helper. - evm/{mod,config}.rs: removed `MinerTrieDbPrefetcher` type alias, `parent_difflayers` / `triedb_prefetcher` fields on `BscNextBlockEnvAttributes` + `BscBlockExecutionCtx`, and the `create_block_builder` workaround that scrubbed them. - engine.rs: dropped the `!is_triedb_active()` gate on the sparse-trie spawner; `BscBuiltPayload::executed_block` no longer carries a difflayer (upstream `ExecutedBlock.difflayer` field deleted). - miner/payload.rs: removed `parent_difflayers` from `BscBuildArguments`, the `fetch_triedb_difflayers` helper, the prefetcher state-hook closures, the pathdb safety guard, and the `executed_block.difflayer` assignments. - miner/bid_simulator.rs: removed the triedb branch from `bid_simulate` and its `parent_difflayers` parameter. - miner/bsc_miner.rs: deleted `fetch_parent_difflayers_for_bid` and its caller. - Removed the now-unused `task_executor` field from `BscPayloadBuilder`, `BidSimulator`, and `MainWorkWorker` (it existed only to spawn the triedb prefetcher). - README.md / CLAUDE.md / main.rs / miner/config.rs: purged `--statedb.triedb` references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: reth dep (#366) Co-authored-by: joey <10592664+joey0612@users.noreply.github.com> Co-authored-by: cbh876 <3930922419@qq.com> Co-authored-by: cbh876 <30834644+constbh@users.noreply.github.com> * chore: remove triedb (rust-eth-triedb/pathdb) state backend Drops all triedb code paths and the four rust-eth-triedb* git deps so the downstream reth-bsc crate compiles against the upstream Reth tree where PR bnb-chain/reth#202 has removed the same backend. After this change the only state backend is MDBX. Pinned `bnb-chain/reth` to rev 6dcb09c0090e046eea6440fbeb58cce5db0476b2 (PR 202 head) and aligned the bsc-ef-tests crate to the same rev. - evm/builder.rs: collapsed `is_triedb_active()` + finish_with_difflayer override into `fn finish`, keeping the sparse-trie precomputed-root fast path and the MDBX `state_root_with_updates` fallback. Removed the `request_difflayer` helper. - evm/{mod,config}.rs: removed `MinerTrieDbPrefetcher` type alias, `parent_difflayers` / `triedb_prefetcher` fields on `BscNextBlockEnvAttributes` + `BscBlockExecutionCtx`, and the `create_block_builder` workaround that scrubbed them. - engine.rs: dropped the `!is_triedb_active()` gate on the sparse-trie spawner; `BscBuiltPayload::executed_block` no longer carries a difflayer (upstream `ExecutedBlock.difflayer` field deleted). - miner/payload.rs: removed `parent_difflayers` from `BscBuildArguments`, the `fetch_triedb_difflayers` helper, the prefetcher state-hook closures, the pathdb safety guard, and the `executed_block.difflayer` assignments. - miner/bid_simulator.rs: removed the triedb branch from `bid_simulate` and its `parent_difflayers` parameter. - miner/bsc_miner.rs: deleted `fetch_parent_difflayers_for_bid` and its caller. - Removed the now-unused `task_executor` field from `BscPayloadBuilder`, `BidSimulator`, and `MainWorkWorker` (it existed only to spawn the triedb prefetcher). - README.md / CLAUDE.md / main.rs / miner/config.rs: purged `--statedb.triedb` references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: const <122766871+constwz@users.noreply.github.com> Co-authored-by: joey <10592664+joey0612@users.noreply.github.com> Co-authored-by: cbh876 <3930922419@qq.com> Co-authored-by: cbh876 <30834644+constbh@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull Request ReviewThis PR removes all TrieDB-related functionality and dependencies from a Rust Ethereum client codebase, standardizing state handling on the MDBX backend across engine, storage, pipeline, RPC, CLI, and node-launch paths. It deletes triedb-specific config/CLI flags, prefetchers, alignment/unwind logic, diff-layer plumbing, and conditional runtime branches, while simplifying many call sites (e.g., Sensitive ContentNo sensitive content detected. Security IssuesNo serious security issues detected. Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits. |
* Merge pull request #375 from bnb-chain/feat/pasteur-fork-definition feat(hardforks): define Pasteur hardfork * feat(evm/precompiles): add Pasteur precompile set + dedup utilities (#376) Introduce a dedicated Pasteur precompile set (build_pasteur_precompiles / pasteur_traced / pasteur()) wired into the dispatcher, currently identical to the Mendel set. Add a shared dedup module (DuplicateTracker + is_zero_bytes, ported from bnb-chain/bsc lightclient/v2) that later PRs use to reject duplicate validator identities and signer pubkeys. Enabler only — no behavior change yet. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(evm/precompiles): reject duplicate validators in cometBFT 0x67 at Pasteur (#377) Add the Pasteur cometBFT light-block precompile (0x67), which rejects validator sets containing duplicate identities. require_unique_validators is threaded through the decode path and validate_unique_validator_set checks uniqueness of address, consensus pubkey, BLS key and relayer address across both the trusted consensus state and the incoming light-block validator set. Unset (zero/empty) bridge fields are exempt. Pre-Pasteur behavior is unchanged. Ports bnb-chain/bsc #3623 (cometBFT light-block path). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(evm/precompiles): suspend legacy v1 Tendermint precompiles + reprice 0x67 at Pasteur (#379) From Pasteur, the legacy v1 Tendermint header (0x64) and IAVL proof (0x65) precompiles are deprecated and return an error for any input, and the cometBFT light-block precompile (0x67) is repriced with per-input-byte gas (base 3000 + 16/byte) so cost scales with the validator/signature count. Pre-Pasteur behavior (live 0x64/0x65, flat 3000 for 0x67) is unchanged. Ports bnb-chain/bsc #3726. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(system-contracts): Pasteur StakeHub + Governor upgrade (#380) * feat(system-contracts): Pasteur StakeHub + Governor upgrade Add the Pasteur system-contract upgrade: StakeHub (0x2002) and Governor (0x2004) are swapped to genesis-contract v1.2.6 bytecode at the Pasteur transition on mainnet/chapel/rialto. Adds the per-network bytecode, registers `pasteur` in build.rs (which regenerates embedded_contracts.rs), and wires Pasteur into hardfork_to_dir_name, hardforks_with_system_contracts, and get_upgrade_system_contracts. Ports bnb-chain/bsc #3721 + #3727. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(system-contracts): cover Pasteur upgrade at the transition boundary Add a test driving get_upgrade_system_contracts across the Pasteur transition: the upgrade fires only when the parent is pre-Pasteur and the block is at/after it, swapping exactly StakeHub and Governor; blocks fully before or after the fork emit no Pasteur upgrade. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: remove triedb (rust-eth-triedb/pathdb) state backend (#371) * chore: reth dep (#366) Co-authored-by: joey <10592664+joey0612@users.noreply.github.com> Co-authored-by: cbh876 <3930922419@qq.com> Co-authored-by: cbh876 <30834644+constbh@users.noreply.github.com> * chore: remove triedb (rust-eth-triedb/pathdb) state backend Drops all triedb code paths and the four rust-eth-triedb* git deps so the downstream reth-bsc crate compiles against the upstream Reth tree where PR bnb-chain/reth#202 has removed the same backend. After this change the only state backend is MDBX. Pinned `bnb-chain/reth` to rev 6dcb09c0090e046eea6440fbeb58cce5db0476b2 (PR 202 head) and aligned the bsc-ef-tests crate to the same rev. - evm/builder.rs: collapsed `is_triedb_active()` + finish_with_difflayer override into `fn finish`, keeping the sparse-trie precomputed-root fast path and the MDBX `state_root_with_updates` fallback. Removed the `request_difflayer` helper. - evm/{mod,config}.rs: removed `MinerTrieDbPrefetcher` type alias, `parent_difflayers` / `triedb_prefetcher` fields on `BscNextBlockEnvAttributes` + `BscBlockExecutionCtx`, and the `create_block_builder` workaround that scrubbed them. - engine.rs: dropped the `!is_triedb_active()` gate on the sparse-trie spawner; `BscBuiltPayload::executed_block` no longer carries a difflayer (upstream `ExecutedBlock.difflayer` field deleted). - miner/payload.rs: removed `parent_difflayers` from `BscBuildArguments`, the `fetch_triedb_difflayers` helper, the prefetcher state-hook closures, the pathdb safety guard, and the `executed_block.difflayer` assignments. - miner/bid_simulator.rs: removed the triedb branch from `bid_simulate` and its `parent_difflayers` parameter. - miner/bsc_miner.rs: deleted `fetch_parent_difflayers_for_bid` and its caller. - Removed the now-unused `task_executor` field from `BscPayloadBuilder`, `BidSimulator`, and `MainWorkWorker` (it existed only to spawn the triedb prefetcher). - README.md / CLAUDE.md / main.rs / miner/config.rs: purged `--statedb.triedb` references. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: const <122766871+constwz@users.noreply.github.com> Co-authored-by: joey <10592664+joey0612@users.noreply.github.com> Co-authored-by: cbh876 <3930922419@qq.com> Co-authored-by: cbh876 <30834644+constbh@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump reth dep to latest remove_triedb (PR #202) commit 0dea17d2 Picks up reth#204 "persist target clamp finalized" on top of the remove_triedb branch (was 6dcb09c). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(miner): adaptive root-reserve + per-attempt sparse-trie sink (ported to develop-hardfork-pasteur) (#418) * chore: update pasteur time in qa env * test: fix Pasteur dormancy test after qanet activation scheduled qanet now schedules Pasteur at a real timestamp, so the test must assert its concrete activation instead of dormancy. Mainnet/testnet remain dormant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rpc/eth_config): return null blobSchedule to match go-bsc (#422) go-bsc's eth_config (EIP-7910) returns `blobSchedule: null` for forks whose ChainConfig.BlobConfig(LatestFork(t)) yields nil (e.g. Mendel, Pasteur), while reth returned a populated object — diverging from the reference client. reth's eth_config uses alloy's EthConfig, whose EthForkConfig.blob_schedule is a required (non-optional) BlobParams that always serializes, so it cannot emit null. Introduce a BSC-specific eth_config RPC (BscEthConfigApi) that returns a raw JSON value, then null `blobSchedule` on current/next/last per go-bsc's per-fork logic: keep it only for Cancun/Fermi/Maxwell/Lorentz/Osaka; null for all others (incl. Mendel/Pasteur). Each snapshot's fork is resolved from its activationTime via revm_spec_by_timestamp_and_block_number. RPC introspection only — no consensus impact (both clients enforce the same blob params in block validation). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: qanet hf time * test: expect Pasteur dormant on qanet after activation reverted The qanet Pasteur activation was reverted to unscheduled (u64::MAX) in cf8d5d1, but the dormancy test still expected the old concrete timestamp. Fold qanet into the dormant-schedule assertion loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: const <122766871+constwz@users.noreply.github.com> Co-authored-by: joey <10592664+joey0612@users.noreply.github.com> Co-authored-by: cbh876 <3930922419@qq.com> Co-authored-by: cbh876 <30834644+constbh@users.noreply.github.com>
…to db tip Ports the net-new correctness fixes from #202 / #204 (chee-chyuan) onto the v2.4.1 state-root/changeset-cache redesign, since the triedb-removal half of #202 is already covered by the v2.4.1 upgrade. - engine tree: clamp the Threshold persist target to the finalized block so losing forks stay in memory (avoids expensive on-disk trie reverts that stall the engine under load). - trie-db changesets: bound the tail revert read to db_tip_block instead of the unbounded `next_block..`; add get_or_compute_range_tracked reporting whether an aggregate DB fallback ran. - provider overlay: when the changeset cache fell back to a DB recompute, re-read the db tip and discard the overlay (InsufficientChangesets) if it moved, since a concurrent persist rewrites static-file changesets outside the MDBX snapshot. Adapted to v2.4.1's range-aggregate changeset cache (vs #204's per-block iteration). Metrics counters from #204 omitted to avoid a new metrics feature on reth-trie-db. Original: 0dea17d (cherry picked, reworked). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Merge pull request #375 from bnb-chain/feat/pasteur-fork-definition
feat(hardforks): define Pasteur hardfork
* feat(evm/precompiles): add Pasteur precompile set + dedup utilities (#376)
Introduce a dedicated Pasteur precompile set (build_pasteur_precompiles /
pasteur_traced / pasteur()) wired into the dispatcher, currently identical to
the Mendel set. Add a shared dedup module (DuplicateTracker + is_zero_bytes,
ported from bnb-chain/bsc lightclient/v2) that later PRs use to reject duplicate
validator identities and signer pubkeys.
Enabler only — no behavior change yet.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(evm/precompiles): reject duplicate validators in cometBFT 0x67 at Pasteur (#377)
Add the Pasteur cometBFT light-block precompile (0x67), which rejects validator
sets containing duplicate identities. require_unique_validators is threaded
through the decode path and validate_unique_validator_set checks uniqueness of
address, consensus pubkey, BLS key and relayer address across both the trusted
consensus state and the incoming light-block validator set. Unset (zero/empty)
bridge fields are exempt. Pre-Pasteur behavior is unchanged.
Ports bnb-chain/bsc #3623 (cometBFT light-block path).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(evm/precompiles): suspend legacy v1 Tendermint precompiles + reprice 0x67 at Pasteur (#379)
From Pasteur, the legacy v1 Tendermint header (0x64) and IAVL proof (0x65)
precompiles are deprecated and return an error for any input, and the cometBFT
light-block precompile (0x67) is repriced with per-input-byte gas
(base 3000 + 16/byte) so cost scales with the validator/signature count.
Pre-Pasteur behavior (live 0x64/0x65, flat 3000 for 0x67) is unchanged.
Ports bnb-chain/bsc #3726.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(system-contracts): Pasteur StakeHub + Governor upgrade (#380)
* feat(system-contracts): Pasteur StakeHub + Governor upgrade
Add the Pasteur system-contract upgrade: StakeHub (0x2002) and Governor (0x2004)
are swapped to genesis-contract v1.2.6 bytecode at the Pasteur transition on
mainnet/chapel/rialto. Adds the per-network bytecode, registers `pasteur` in
build.rs (which regenerates embedded_contracts.rs), and wires Pasteur into
hardfork_to_dir_name, hardforks_with_system_contracts, and
get_upgrade_system_contracts.
Ports bnb-chain/bsc #3721 + #3727.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(system-contracts): cover Pasteur upgrade at the transition boundary
Add a test driving get_upgrade_system_contracts across the Pasteur transition:
the upgrade fires only when the parent is pre-Pasteur and the block is at/after
it, swapping exactly StakeHub and Governor; blocks fully before or after the fork
emit no Pasteur upgrade.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): add BEP-675 block-MEV-info header tagging (#381)
Port core/types/block_mev_info.go: encode/decode the winning MEV path
(SendBid=1, SendBidBlock=2) and builder address into a 32-byte tag for
header.requests_hash. Locally-built blocks keep the empty requests hash, so
"untagged" == local.
First foundation slice of the BEP-675 builder-block work (bnb-chain/bsc #3691);
consumed by later BidBlock sub-PRs.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): BEP-675 BidBlock builder permission manager (PR 8c) (#382)
* feat(miner): add BEP-675 BidBlock builder permission manager
Port miner/bid_block_permission.go + core/types/bid_block_permission.go:
BidBlockPermissionManager tracks per-builder SendBidBlock revokes in memory with
lazy expiry, plus BidBlockPermissionStatus for the permission RPC. Builders are
revoked for a lockout window (default 24h; 450s for gas-price policy; manual via
set_allowed) and become allowed again once the window passes.
Self-contained slice of the BEP-675 builder-block work (bnb-chain/bsc #3691);
consumed by the miner BidBlock path and permission RPC sub-PRs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(miner): expand BidBlock permission manager coverage
Port the additional manager-level cases from bnb-chain/bsc
miner/bid_block_permission_test.go: per-builder independence, revoke overwrite,
elapsed-time (not wall-day) expiry, independent reset times across builders,
active-revoke-count staleness, full GetStatus detail, and concurrent access.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(parlia): add BEP-675 BidBlock system-tx validation (#383)
Port the pure validation logic from consensus/parlia/bid_block.go: classify
unsigned system-tx candidates, the signable whitelist (deposit /
distributeFinalityReward / updateValidatorSetV2 on the Validator contract),
the expected trailing-system-tx shape for a header (deposit; +finality reward
every 200 blocks; +validator-set update on breathe blocks), shape verification,
and deposit-value extraction.
Parameterized on (txs, header, parent) so it doesn't depend on the deferred
hash-critical BidBlock types. The stateful Parlia methods
(prepare/finalize/assemble, block-time check, sign wrapper) are a later slice.
Slice of the BEP-675 builder-block work (bnb-chain/bsc #3691).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): add BEP-675 BidBlock types (hash verified against geth) (#384)
Port core/types/bid.go: BidBlock, BidBlockArgs, DecodedBidBlock. BidBlock::hash
reproduces geth's rlpHash([header, transactions, sidecars]) and ecrecover_sender
recovers the builder from the signature over that hash.
Hash and recovery are validated against vectors generated from go-ethereum
(BidBlock.Hash + EcrecoverSender), so builder signatures recover cross-client.
hash() is vector-verified for the no-blob case (empty sidecars); non-empty
blob-sidecar parity is a flagged follow-up.
Slice of the BEP-675 builder-block work (bnb-chain/bsc #3691); unblocks the
miner BidBlock path and the mev_sendBidBlock RPC.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): add BidBlockEnabled config + expose via mev_params (#385)
Add MiningConfig::bid_block_enabled (BEP-675), off by default, settable via
--mining.bid-block-enabled (CLI > env BSC_MINING_BID_BLOCK_ENABLED > default).
Expose it on MevParams as "BidBlockEnabled" so builders can query whether
mev_sendBidBlock is accepted, matching geth-bsc.
Gating prerequisite for the BidBlock RPC admission path (bnb-chain/bsc #3691).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(mev): mev_sendBidBlock admission path (BEP-675, PR 8e) (#386)
* feat(mev): add mev_sendBidBlock admission path (BEP-675, PR 8e)
Adds the mev_sendBidBlock RPC entry. Mirrors go-bsc's
MevAPI.SendBidBlock + Miner.SendBidBlock admission: enabled-gate
(MEV running AND BidBlockEnabled AND Pasteur active at head) →
head-alignment / in-turn → builder recovery, whitelist & permission
→ trailing system-tx validation. Acknowledges the bid hash on
success; the validator-side build/simulation enqueue lands in 8d.
Adds serde to BidBlock/BidBlockArgs (geth wire parity) and a global
BidBlockPermissionManager shared with the miner's auto-revoke path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(mev): align send_bid_block with go-bsc admission boundary
Mirror go-bsc's two functions in their actual order — MevAPI.SendBidBlock
(MEV-running + structural checks) then the front of Miner.SendBidBlock
(enabled-gate, builder recovery, whitelist, permission). Drop the inline
system-tx verification: in go-bsc that lives at the end of
preSealVerifyBidBlock (header/gasLimit/gasFee/blob checks first), which is
the 8d build path, so verifying it alone here was misplaced and partial.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(mev): match go-bsc bid-error codes and two-layer SendBidBlock split
Two concrete divergences from go-bsc #3691:
- Error codes: use the dedicated bid-error range from
core/types/bid_error.go (-38001 InvalidBidParam, -38003 MevNotRunning,
-38005 MevNotInTurn, -38006 BidBlockPermissionRevoked) with geth's exact
messages, instead of generic -32602/-32603. Builders parse these codes.
- Structure: split the merged handler to mirror geth's two functions —
send_bid_block (MevAPI.SendBidBlock: MEV-running + structural) delegates
to admit_bid_block (front of Miner.SendBidBlock: gate, recover,
whitelist, permission). Simulator-backed tail still deferred to 8d.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(parlia): add BlockTimeUpperCheck for BidBlock pre-seal (PR 8b-2) (#387)
Port go-bsc Parlia.BlockTimeUpperCheck: a BidBlock header's timestamp must
not exceed block_time_for_ramanujan_fork(parent) — the same in-turn bound
the validator uses when building locally — so a builder cannot post-date a
block beyond its slot. Consumed by BidBlock pre-seal verification (8d).
The remaining parlia BidBlock methods (PrepareForBidBlock,
FinalizeAndAssembleBidBlock, SignSystemTx) are realized through reth-bsc's
payload builder rather than as standalone parlia wrappers, so they land
with the miner build path in 8d.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(miner): verify BidBlock hash with non-empty blob sidecar (#389)
Closes the 8a blob-parity gap: BidBlock::hash() was only vector-verified
for empty sidecars. Adds a Go-generated vector for a BidBlock carrying one
blob sidecar, confirming the RLP layout
[[blobs, commitments, proofs], blockNumber, blockHash, txIndex, txHash]
matches go-bsc byte-for-byte (sidecar Version is rlp:"-", excluded from
the hash). Pure test; no production change.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): BidBlock blob-sidecar validation + version field (PR 8d-1) (#390)
Port go-bsc validateBidBlockBlobSidecars: for each user blob tx, require an
in-order sidecar that exists, is a legacy (v0) proof sidecar, matches by
tx hash + index, and keeps the running blob count within the per-block
max; reject trailing extras. KZG proof verification is left to final block
insertion, matching go-bsc's cheap-checks-at-admission behavior.
Adds the sidecar `version` field to BscBlobTransactionSidecar (mirrors
go-bsc BlobTxSidecar.Version, rlp:"-" — excluded from RLP/hash, JSON-only,
serde default 0) needed for the v1/cell-proof rejection.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): assemble pre_seal_verify_bid_block (PR 8d-1b) (#391)
Port go-bsc bidSimulator.preSealVerifyBidBlock: the cheap pre-seal checks a
validator runs before sealing a builder block, in go-bsc's order —
coinbase == validator, gasLimit == in-turn target, valid unsealed Parlia
header (via Parlia::validate_header, reth-bsc's VerifyUnsealedHeader
equivalent), block_time_upper_check, non-zero deposit gas-fee, blob
sidecars, per-tx gas cap (EIP-7825 MaxTxGas), and the trailing system-tx
region. Returns the located (system_tx_start, gas_fee).
KZG proofs and parent-relative cascading fields are re-verified at block
insertion, matching go-bsc.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): BidBlock intake queue + admission enqueue (PR 8d-2a) (#392)
Wire the merged mev_sendBidBlock admission path into a process-global
BidBlock intake queue (mirrors the legacy SendBid BID_PACKAGE_QUEUE):
admit_bid_block now decodes the admitted block and pushes a
DecodedBidBlock for the miner to consume. Pre-seal verification, execution,
selection against the local block, and revoke-on-invalid run miner-side on
pop (8d-2b), matching reth-bsc's SendBid layering where the RPC enqueues
and the miner verifies/executes.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): bind-sign BidBlock system txs (PR 8d-2b-i) (#393)
Port go-bsc bindSignBidBlockSystemTxs + parlia.SignSystemTx: blind-sign the
trailing unsigned system txs of a verified BidBlock with the validator key
via the existing sign_system_transaction, copying the builder-signed user
txs through unchanged. Signing the validator-owned system txs is the
prerequisite that makes the builder-assembled block sealable.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): BidBlock EVM block-context attribute derivation (PR 8d-2b-ii) (#394)
bid_block_env_attributes derives the EVM payload attributes for executing a
builder-proposed block, preserving the builder's exact block context
(timestamp, coinbase, difficulty->prev_randao, beacon root). On BSC
PREVRANDAO returns the header difficulty, so it is taken verbatim from the
builder's header rather than recomputed from the snapshot as the local
build path does; changing any block-context field would diverge the
re-executed state root (go-bsc prepareBidBlockTask).
This is the correctness-critical, unit-tested core of the execution
adapter. The executor + finalize/seal wiring (which must likewise preserve
the builder's difficulty) is the integration-gated remainder.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ef-tests): BidBlock execution harness scaffold (PR 8d-2b-harness) (#395)
First step of the BEP-675 BidBlock execution harness: stand up a real BSC
execution environment (test provider factory + BSC mainnet genesis with
system contracts) and confirm a state provider opens at the expected
genesis. The trusted local build, simulate_bid_block, and the round-trip
byte-exact assertion build on this foundation.
Test-only; no production change.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ef-tests): signable BSC test genesis for BidBlock harness (PR 8d-2b-genesis) (#396)
Add a signable test chain spec whose sole genesis validator is the Anvil
dev key the miner tests control, so the harness can build and seal blocks
as that validator (bsc_mainnet's validator keys aren't ours). Minimal
hardforks keep the validator encoding pre-Luban (vanity + 20-byte address +
seal). Test confirms the genesis parses to a snapshot with that validator.
Prerequisite for the trusted local build (which must sign system txs as the
validator). Test-only.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ef-tests): wire BidBlock harness execution environment (PR 8d-2b-env) (#397)
Add the globals the BSC executor reads during block execution: an in-memory
MockSnapshotProvider (published via shared::set_snapshot_provider) seeded
with the genesis snapshot, and the global validator signer (the dev key
matching the genesis validator). Test confirms the parent snapshot is
retrievable. Reusable setup for the trusted local build. Test-only.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ef-tests): trusted local block build in BidBlock harness (PR 8d-2b-build) (#398)
Drive BscEvmConfig's builder on the signable genesis to produce block 1
end-to-end (builder_for_next_block -> apply_pre_execution_changes ->
finish_with_difflayer), asserting a sealed block with a real state root.
Required publishing the global header reader (set_header_provider) the BSC
pre/post-execution hooks use to look up the parent. This block is the
reference the round-trip will compare simulate_bid_block against. Test-only.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ef-tests): BidBlock build/execute round-trip agreement (PR 8d-2b-roundtrip) (#402)
Prove the core verify-mode invariant: build block 1 via the builder path,
finalize/seal it (finalize_new_header: difficulty + ECDSA seal), then
re-execute the sealed block via the executor path and assert both paths
agree on the post-state root.
Build uses prev_randao = calculate_difficulty(...) so it matches the
finalized difficulty (BSC PREVRANDAO == difficulty), and a 32-byte vanity so
the seal append reaches the vanity+seal minimum. Publishes a block-1
snapshot (validator set unchanged for this non-epoch block) the executor
looks up post-apply. Test-only.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(miner): split pre_seal_verify into header + payload halves (PR 8d-2b-decompose) (#403)
Decompose pre_seal_verify_bid_block into verify_bid_block_header
(validate_header + block_time_upper_check, must run on the finalized header)
and verify_bid_block_payload (coinbase/gas-limit/gasFee/blob/per-tx/system-tx,
runs before finalize and needs no Parlia engine). pre_seal_verify_bid_block
stays as a wrapper calling both, so behavior is unchanged.
Needed for the BidBlock build path: bind-signing consumes the payload half's
system_tx_start and mutates the tx set, so it must precede finalize, while
header validation must follow it.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): add simulate_bid_block (BEP-675, PR 8d-2b-simfn) (#404)
* feat(miner): add simulate_bid_block (BEP-675, PR 8d-2b-simfn)
The validator-side simulation of an admitted BidBlock: payload-verify
(verify_bid_block_payload) -> blind-sign trailing system txs -> install the
validator's block context (its extra + recomputed tx root, other context
fields preserved) -> finalize/seal (finalize_new_header) -> assemble the
sealed RecoveredBlock. Returns the sealed block + gas_fee + system_tx_start;
execution is left to the caller (env-specific). Composes the pieces proven
in #402 (finalize/execute agreement) and #403 (verify decomposition).
Unit test confirms it produces a sealed block with a 32+65 extra, the
deposit tx validator-signed, and the located gas_fee/system_tx_start.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(miner): rename SimulatedBidBlock -> BidBlockTask
Mirror go-bsc naming: the returned struct maps to geth's task/bidBlockTaskInfo
(block + gasFee + systemTxStart), so name it BidBlockTask.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): tag BidBlock header with MEV info in simulate (PR 8d-2c-mevinfo) (#405)
Port go-bsc setBidMevInfo (BidBlock case): simulate_bid_block now stamps the
header's requests_hash with encode_block_mev_info(BidBlock, builder) when
Prague is active (BidBlock is post-Prague, so requests_hash is present).
Closes the setBidMevInfo gap flagged when comparing simulate to the geth
flow. set_bid_block_mev_info is unit-tested (tagged only when Prague active).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ef-tests): BidBlock execution-gate round-trip (PR 8d-2b-execgate) (#406)
The byte-exact verify-mode proof for simulate_bid_block. On a Kepler-active
signable chain (BSC-typed test provider so BscBlocks insert): build+insert
block 1, build block 2 with a fee-paying user tx -> [user, deposit],
repackage it as a BidBlock with the deposit UNSIGNED, run simulate_bid_block
(which re-signs it + finalizes), execute the sealed result, and assert the
state root equals the reference build.
Kepler skips the distribute_to_system sys-reward (so the trailing region is
exactly [deposit]); building at block 2 avoids block-1 system-contract init;
genesis baseFeePerGas=0 keeps base fee 0 so a gas_price=1 tx funds the
deposit. A shared accumulating mock header provider lets the multi-spec
harness tests coexist on the process-global header/snapshot readers.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): BidBlock consumer loop — pop, simulate, store best (PR 8d-2b-iv) (#407)
Wire the admitted-BidBlock queue into the miner (go-bsc newBidBlockLoop ->
AddBidBlock): MevWorkWorker now pops pop_bid_block_package on each send-bid
tick and calls BidSimulator::commit_bid_block, which derives the build
context (parent header, parent snapshot, gas ceil from the miner config),
runs simulate_bid_block, and keeps the highest-fee sealed BidBlockTask per
parent in a new best_bid_block store (GetBestBidBlock). Adds builder/bid_hash
to BidBlockTask. This gives pop_bid_block_package its production caller.
Execution + selection against the local block + submit (the build-cycle
side) and revoke-on-mismatch are the remaining follow-ons.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): BidBlock build-cycle — execute, verify root, select (PR 8d-2b-v) (#408)
Complete the consumer: commit_bid_block now executes the simulated BidBlock
against the parent state, verifies the builder's claimed state root
(rejecting a dishonest builder via state_root_with_updates), and assembles a
BscBuiltPayload (is_bid=true) into the best-bid-block store. The payload
builder's try_return_best_payload calls collect_best_bid_block, which pushes
it into potential_payloads so it competes with the local block and legacy
SendBid by fee in pick_best_payload_and_finalize (go-bsc selectBidBlock).
End-to-end now: mev_sendBidBlock -> queue -> miner pop -> simulate -> execute
-> select. Remaining: revoke-on-mismatch (8d-2c) + configured vanity.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): revoke dishonest BidBlock builders + configured vanity (PR 8d-2c) (#409)
When an executed BidBlock's computed state root disagrees with the builder's
claimed root, revoke the builder's permission (BidBlockPermissionManager from
#382) — go-bsc's handleBidBlockResult does the same on the InsertChain
mismatch. General simulate/prepare failures stay plain rejections, matching
geth (which only revokes on blob-tx validation, gated here on the unported KZG
path).
Also use the operator-configured extra (miner_setExtra / geth worker.extra) as
the block vanity instead of fixed zero bytes, padded/truncated to the Parlia
vanity length.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: add CheckPending quota and bidMustBefore timing checks to admit_bid_block
* fix(mev): add missing CheckPending quota and bidMustBefore timing checks to admit_bid_block
Mirrors the front half of go-bsc Miner.SendBidBlock that was absent from
the reth-bsc admission path:
- CheckPending: duplicate-bid detection and per-builder-per-block quota
(max_bids_per_builder, default 3). Adds pending_bid_blocks tracking map
to MevApiImpl so quota is maintained entirely at RPC admission time.
Quota is only charged after all other checks pass, matching geth ordering.
- bidMustBefore: rejects bids that arrive after the sealing deadline
(parent_ts_ms + block_interval_ms - no_interrupt_left_over_ms).
Blocks arriving past this point cannot be simulated in time and
would be dropped by the miner anyway.
The deferred tail (Extra overwrite, setBidMevInfo, preSealVerifyBidBlock)
remains miner-side as documented in the existing comment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(mev): clarify deferred admit_bid_block checks vs geth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(mev): fix clippy warnings in admit_bid_block (type alias + redundant closure)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: remove triedb (rust-eth-triedb/pathdb) state backend (#371)
* chore: reth dep (#366)
Co-authored-by: joey <10592664+joey0612@users.noreply.github.com>
Co-authored-by: cbh876 <3930922419@qq.com>
Co-authored-by: cbh876 <30834644+constbh@users.noreply.github.com>
* chore: remove triedb (rust-eth-triedb/pathdb) state backend
Drops all triedb code paths and the four rust-eth-triedb* git deps so the
downstream reth-bsc crate compiles against the upstream Reth tree where
PR bnb-chain/reth#202 has removed the same backend. After this change
the only state backend is MDBX.
Pinned `bnb-chain/reth` to rev 6dcb09c0090e046eea6440fbeb58cce5db0476b2
(PR 202 head) and aligned the bsc-ef-tests crate to the same rev.
- evm/builder.rs: collapsed `is_triedb_active()` + finish_with_difflayer
override into `fn finish`, keeping the sparse-trie precomputed-root
fast path and the MDBX `state_root_with_updates` fallback. Removed
the `request_difflayer` helper.
- evm/{mod,config}.rs: removed `MinerTrieDbPrefetcher` type alias,
`parent_difflayers` / `triedb_prefetcher` fields on
`BscNextBlockEnvAttributes` + `BscBlockExecutionCtx`, and the
`create_block_builder` workaround that scrubbed them.
- engine.rs: dropped the `!is_triedb_active()` gate on the sparse-trie
spawner; `BscBuiltPayload::executed_block` no longer carries a
difflayer (upstream `ExecutedBlock.difflayer` field deleted).
- miner/payload.rs: removed `parent_difflayers` from
`BscBuildArguments`, the `fetch_triedb_difflayers` helper, the
prefetcher state-hook closures, the pathdb safety guard, and the
`executed_block.difflayer` assignments.
- miner/bid_simulator.rs: removed the triedb branch from
`bid_simulate` and its `parent_difflayers` parameter.
- miner/bsc_miner.rs: deleted `fetch_parent_difflayers_for_bid` and
its caller.
- Removed the now-unused `task_executor` field from
`BscPayloadBuilder`, `BidSimulator`, and `MainWorkWorker` (it
existed only to spawn the triedb prefetcher).
- README.md / CLAUDE.md / main.rs / miner/config.rs: purged
`--statedb.triedb` references.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: const <122766871+constwz@users.noreply.github.com>
Co-authored-by: joey <10592664+joey0612@users.noreply.github.com>
Co-authored-by: cbh876 <3930922419@qq.com>
Co-authored-by: cbh876 <30834644+constbh@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump reth dep to latest remove_triedb (PR #202) commit 0dea17d2
Picks up reth#204 "persist target clamp finalized" on top of the
remove_triedb branch (was 6dcb09c).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf(miner): adaptive root-reserve + per-attempt sparse-trie sink (ported to develop-hardfork-pasteur) (#418)
* feat(miner): switch BidBlock to zero-simulate (broadcast-then-verify)
Make the BEP-675 BidBlock path faithful to go-bsc PR #3691: the validator
no longer re-executes the builder's block before proposing it. Instead it
blind-signs + seals at intake, selects by fee, broadcasts the sealed block,
and only then executes + state-root-verifies via engine.new_payload (the
peer-block path = InsertChain), revoking the builder on Invalid.
- bid_simulator: commit_bid_block stores the sealed (unexecuted) BidBlockTask
by fee; remove execute_bid_block_payload; best_bid_block() returns the task.
- payload: collect the unexecuted candidate; route a fee-winning bid to the
import service and discard local work (go-bsc selectBidBlock + commitWork).
- block_import/service: new IncomingBidBlock channel + on_new_bid_block
(broadcast first, verify via engine.new_payload, revoke on Invalid).
- shared/network: wire the BidBlock import sender.
State-root-mismatch revoke moves from intake to the post-broadcast import path
(go-bsc handleBidBlockResult). Blob KZG validation remains unported (pre-existing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(miner): verify BidBlock blob KZG proofs before broadcast
Port go-bsc's pre-broadcast blob KZG gate (prepareBidBlockTask ->
validateBidBlockBlobTxs -> txpool.ValidateBlobTx). Under zero-simulate, full
re-execution is deferred to after broadcast, so without this a block with bad
blob proofs would be gossiped before rejection.
validate_bid_block_blob_kzg runs the EIP-4844 proof check on the winning block
in try_submit_winning_bid_block, before it is handed to the import service. On
failure the builder is revoked and the validator falls back to the local
payload (go-bsc bidBlockFallback), so a bad-blob block is never proposed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(miner): cover zero-simulate BidBlock import + blob KZG gate
- bid_block: unit-test validate_bid_block_blob_kzg — accepts no-blob blocks,
rejects a blob tx with no sidecar (MissingSidecar), and rejects a bogus
sidecar (Invalid) returning a typed error rather than panicking.
- service: bid_block_broadcasts_then_revokes_on_invalid drives on_new_bid_block
through the mock-engine harness, asserting the block is broadcast (ValidBlock
announcement) before verification, then the builder is revoked when the
engine returns Invalid (go-bsc handleBidBlockResult).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(miner): discard stale BidBlocks at consumer + document running/receiving gate
Mirrors go-bsc's newBidBlockLoop stale-block-number check: admission only
gates on head-relative timing, so a bid admitted just before the head
advances can still reach commit_bid_block for a block we have already
passed. Early-return with a debug log instead of running through verify
+ execute for nothing.
The companion comment records why we intentionally omit go-bsc's
isRunning / receivingBid re-check: is_mev_running is already enforced at
admission, and bid_receiving has no runtime toggle in this codebase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: update pasteur time in qa env
* test: fix Pasteur dormancy test after qanet activation scheduled
qanet now schedules Pasteur at a real timestamp, so the test must assert
its concrete activation instead of dormancy. Mainnet/testnet remain dormant.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(clippy): allow too_many_arguments on ImportService::new
The zero-simulate BidBlock channel added an 8th parameter to
ImportService::new, tripping clippy::too_many_arguments under
-D warnings in CI. Annotate the constructor to allow it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(rpc/eth_config): return null blobSchedule to match go-bsc (#422)
go-bsc's eth_config (EIP-7910) returns `blobSchedule: null` for forks whose
ChainConfig.BlobConfig(LatestFork(t)) yields nil (e.g. Mendel, Pasteur), while
reth returned a populated object — diverging from the reference client.
reth's eth_config uses alloy's EthConfig, whose EthForkConfig.blob_schedule is a
required (non-optional) BlobParams that always serializes, so it cannot emit
null. Introduce a BSC-specific eth_config RPC (BscEthConfigApi) that returns a
raw JSON value, then null `blobSchedule` on current/next/last per go-bsc's
per-fork logic: keep it only for Cancun/Fermi/Maxwell/Lorentz/Osaka; null for
all others (incl. Mendel/Pasteur). Each snapshot's fork is resolved from its
activationTime via revm_spec_by_timestamp_and_block_number.
RPC introspection only — no consensus impact (both clients enforce the same
blob params in block validation).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: qanet hf time
* fix(miner): accept geth's unsigned-system-tx wire format and fix bidMustBefore deadline
BidBlockArgs::decode_txs rejected go-bsc's V=R=S=0 legacy encoding for
unsigned trailing system txs (alloy's decoder only accepts v in {27,28,35+}),
so every real BidBlock failed to decode before any BEP-675 validation ran.
Add a strict fallback decoder that accepts only that exact placeholder shape.
bid_must_before_ms truncated the parent's sub-second millisecond component
and subtracted no_interrupt_left_over (500ms, meant for bid simulation)
instead of a delayLeftOver equivalent (go-bsc default 15ms), which pushed
the admission deadline at or before the parent's own timestamp on Fermi/
Maxwell's sub-second block intervals — rejecting nearly all BidBlocks as
"too late". Add a dedicated delay_left_over config knob and compute the
deadline from the parent's full millisecond timestamp.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(miner): close double-sign gap on the winning BidBlock path
The local/legacy payload path funnels through ResultWorkWorker::submit_payload,
which guards against signing two different blocks at the same height/parent
(recent_mined_blocks LRU) and re-checks that the parent is still canonical
after the build window. try_submit_winning_bid_block sent a winning BidBlock
straight to the import channel, bypassing both checks — a reorg during the
build window, or a competing block already produced at the same height,
could let the validator sign and broadcast two blocks for the same slot.
Move the double-sign cache into a shared global (crate::shared::
check_and_record_mined_block) so both submission paths record into and check
the same state, and give try_submit_winning_bid_block the same late
canonical-parent / best-block-number re-checks. The double-sign record now
happens immediately before the point of no return, with a rollback
(forget_recorded_mined_block) if the send itself fails, so a legitimate
local-payload fallback for the same slot isn't wrongly blocked.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(node): match go-bsc's BlobSidecar JSON wire format
BscBlobTransactionSidecar derived Serialize/Deserialize, producing
{inner, block_number, block_hash, tx_index, tx_hash, version} — snake_case,
with the inner sidecar under "inner". go-bsc's BlobSidecar has custom
MarshalJSON/UnmarshalJSON that nests blobs/commitments/proofs (plus version,
which Go carries on the inner BlobTxSidecar) under "blobSidecar", with
"blockNumber"/"txIndex" as hex-quantity strings rather than plain numbers.
Any geth-conformant builder submitting a blob-carrying BidBlock failed to
deserialize at the RPC boundary as a result.
Replace the derive with hand-written impls that produce the exact wire
shape, via a small hex_quantity serde module for the two quantity fields.
RLP encoding/hashing (and therefore the BidBlock signature hash) is
unaffected — verified against the existing geth RLP vector test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(miner): add post-import average-gas-price floor check for BidBlocks
go-bsc's validateBidBlockAverageGasPrice runs after InsertChain succeeds,
computing the average gas price over the BidBlock's non-system-tx receipts
and revoking the builder for one epoch if it falls below the validator's
configured floor. This is the primary anti-gaming control for the
deposit-based fee model: without it a builder can pad GasFee via the trailing
system deposit while filling the user-tx region with near-zero-gas-price
transactions, and reth-bsc had no equivalent check at all — the revoke
duration constant existed but was never called from production code.
Add validate_bid_block_average_gas_price + non_system_gas_used (ported from
validateBidBlockAverageGasPrice/calcNonSystemGasUsed) and wire them into
on_new_bid_block's Valid-payload branch, which now fetches receipts via a new
ReceiptProvider bound on ImportService and revokes the builder without
rejecting the already-canonical block, matching go-bsc. gas_fee and
system_tx_start are threaded through the IncomingBidBlock channel type so the
check has what it needs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(miner): add pre-seal cascading header checks for BidBlocks
go-bsc's preSealVerifyBidBlock runs VerifyUnsealedHeader synchronously at
admission, catching a wrong difficulty (in-turn/no-turn mismatch), an
unauthorized or too-recently-signed coinbase, and a too-early timestamp
before the validator commits to signing anything. reth-bsc's
verify_bid_block_header only ran the standalone header check plus the upper
timestamp bound, so those defects were only caught later via
engine.new_payload — after the validator had already signed and broadcast
the block.
Port the cascading checks against the parent snapshot (authorized validator,
sign-recently, difficulty-matches-in-turn) using header.beneficiary directly
rather than recovering a seal signer, since go-bsc's VerifyUnsealedHeader is
designed to run before a valid seal exists. Add the lower Ramanujan
timestamp bound alongside the existing upper bound, with its own new unit
tests since block_time_verify_for_ramanujan_fork had none before.
Fixing this also surfaced an existing ordering bug: pre_seal_verify_bid_block
ran the header cascading checks before the coinbase/gas-limit checks,
backwards from go-bsc's order, so a wrong-coinbase header would have
surfaced UnauthorizedValidator instead of the more specific InvalidCoinbase.
Extracted verify_bid_block_coinbase_and_gas_limit and reordered to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(miner): refresh BidBlock vote attestation at selection, not admission
go-bsc defers assembleVoteAttestation and the ECDSA seal to engine.Seal(),
called only after selectBidBlock picks the winner, maximizing the window for
BFT votes to arrive. reth-bsc assembled a BidBlock's vote attestation at
admission time (simulate_bid_block, when the bid is popped off the intake
queue) -- potentially a full slot before it's known to win -- while the
local-block path already correctly deferred this to finalize_payload at
selection. This silently degraded fast-finality vote coverage specifically
for BidBlock-won blocks.
Add refresh_and_reseal_bid_block, called from try_submit_winning_bid_block
right after a BidBlock wins selection. It calls the pre-existing (until now
dead-code) refresh_vote_attestation_and_seal to strip the admission-time
attestation, reassemble it with the freshest votes, and re-seal. Since the
seal is part of extra_data and therefore the header hash, a successful
refresh changes the block hash; the blob sidecars' cached block_hash (set at
admission time) is patched to match, mirroring finalize_payload's equivalent
patch for the local-block path. Falls back to the admission-time seal
without failing the submission if the snapshot provider is unavailable or
the refresh hits an internal error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(miner): evict stale entries from best_bid_block on clear()
BidSimulator::clear() prunes best_bid_to_run, simulating_bid, best_bid, and
pending_bid by block-number threshold on every chain-head tick, but never
touched best_bid_block -- so every parent hash that ever received an
admitted BidBlock kept its full sealed block, including blob data, in memory
for the lifetime of the process. go-bsc's clearLoop prunes bestBidBlock the
same way as its other maps.
Add retain_recent_bid_blocks, extracted as a free function so it's testable
without constructing a full BidSimulator (which needs generic Client/Pool
bounds), and call it from clear() alongside the existing three maps.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(rpc): add mev_getBidBlockPermission and admin_setBidBlockPermission
Builders revoked via any path (KZG failure, InsertChain-equivalent import
failure, gas-price policy) had no way to query why or when they'd be
re-allowed, and operators couldn't manually clear a revoke without
restarting the node. The underlying BidBlockPermissionManager already
supported both operations (get_status/set_allowed) -- only the RPC surface
was missing.
Add mev_getBidBlockPermission to the existing mev namespace, returning a
BidBlockPermissionResult that mirrors go-bsc's wire shape: detail fields are
omitted entirely when allowed (matching Go's omitempty tags), blockNumber is
a hex-quantity string, and revokedAt/resetAt are RFC 3339 UTC timestamps via
a small dependency-free Unix-seconds-to-RFC3339 converter, matching Go's
default time.Time JSON marshaling.
Add admin_setBidBlockPermission in a new admin.rs module, following this
repo's per-namespace-per-file convention, merged into the RPC router the
same way every other custom namespace already is.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(rpc): return geth's -38007/-38008 codes, verify BidBlock payload synchronously
geth returns -38008 for a too-late bid and -38007 for a synchronous
pre-seal-verification failure, but reth-bsc collapsed both into the generic
-38001 and, for pre-seal failures, admitted the bid optimistically and
dropped it silently later -- builders keying retry/fallback behavior on the
specific geth codes would misbehave, and got no synchronous feedback on an
invalid submission.
Fix the too-late code to -38008. Bring verify_bid_block_payload (coinbase,
gas limit, deposit-derived gas fee, blob-sidecar structure, per-tx gas cap,
trailing system-tx shape) forward to run synchronously at admission,
returning -38007 immediately on failure and populating the DecodedBidBlock's
system_tx_start/gas_fee on success before it's queued.
verify_bid_block_header's structural + cascading checks are deliberately
left deferred to the miner side: go-bsc only makes those checks meaningful
by first overwriting the builder's Extra with the validator's own
reconstructed vanity/forkhash/validator-list/turnLength (SetExtraData, run
before preSealVerifyBidBlock in Go) -- replicating that rewrite at admission
would risk rejecting legitimate submissions whose raw Extra doesn't yet
match the final structure. -38004 (ErrMevBusy) is also left as a known gap:
it requires converting the intake queue from unbounded to
bounded-with-backpressure, a distinct change from correcting error codes
for checks that already run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(config): align BidSimulationLeftOver/NoInterruptLeftOver defaults, make mev_params.BidBlockEnabled dynamic
Two of the smaller config-default divergences flagged in the BEP-675
review: BidSimulationLeftOver defaulted to 50ms (go-bsc: 20ms) and
NoInterruptLeftOver to 500ms (go-bsc's computed default at its default
GasCeil: 110ms bid-processing estimate + 10ms buffer + 15ms delayLeftOver =
135ms). Align both to go-bsc's values.
mev_params.BidBlockEnabled echoed the static config flag rather than
computing it the way admission actually does (mev running && flag &&
Pasteur-active-at-head), so a builder could be told the feature is enabled
right before every submission gets rejected with "disabled". Compute it the
same way admit_bid_block does, falling back to false if the chain head
isn't available yet rather than failing the whole params() call.
Left alone: BidBlockEnabled's own default (false vs Go's true -- BEP-675 is
a still-maturing path this review found and fixed several real bugs in,
so defaulting it off looks intentional), GasCeil (Rust's chain-aware
140M/100M/40M vs Go's flat, likely-outdated 55M), and BuilderFeeCeil (an
economic default, not a timing knob) -- none of these are safe "just align
the number" fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(miner): decode go-bsc unsigned system txs in BidBlock
Trailing system txs in a BidBlock are untagged legacy txs with V=R=S=0
(go-bsc's types.NewTransaction leaves signature fields nil). alloy's
legacy decoder rejects v=0, so decode_2718 fails before any BidBlock
validation runs. Add decode_bid_block_tx: try the standard EIP-2718
decoder first, fall back to a strict all-zero-signature legacy decoder
that only accepts the exact geth placeholder shape. Report the standard
decoder's error on failure. Covered by tests for the geth wire format,
rejection of nonzero r/s with v=0, and regression on normal signed txs.
* fix(bep675): tighten unsealed-header validation, MEV requests_hash gate, bind-sign chain ID, and blob decode safety
- Split HeaderValidator::validate_header into validate_unsealed_header_fields
(all standalone field checks) plus a wall-clock future-timestamp check that
stays in the trait impl. verify_bid_block_header now calls the unsealed
variant since BidBlock headers are validated before the local wall clock
is a meaningful bound.
- Add requests_hash_ok in node/consensus.rs so post-execution validation
accepts headers whose requests_hash was overwritten with a BlockMEVInfo
tag (MEV-won blocks, local or BidBlock) instead of rejecting every
MEV-tagged header as invalid.
- bind_sign_bid_block_system_txs now takes the chain_id and stamps it onto
the unsigned legacy tx before signing, fixing cross-chain replay ambiguity
in the blind-signed trailing system txs.
- Delay releasing a sealed BidBlock until its own header timestamp
(mirrors go-bsc's Parlia.Seal wait-for-slot behavior) instead of
broadcasting immediately after signing.
- In the bid-block import service, run forkchoice update before the
gas-price-floor check and retry the receipts lookup (canonicalized
receipts are only visible via the provider after FCU completes).
- Harden BscBlobTransactionSidecar deserialization: decode blobs/
commitments/proofs via Vec<String> + a heap-allocated hex decode helper
instead of deserializing large FixedBytes<N> arrays directly, avoiding a
remotely-triggerable stack overflow on a single 128KiB blob in debug
builds.
- Add a BSC_DEBUG_BUILDER-gated debug_buildCandidateBlock RPC endpoint
(src/rpc/debug_builder.rs) for locally exercising the BidBlock build path
against real state without a live builder.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(miner): give bid simulation the full block-interval delay window
Port go-bsc PR #3669 (fix 2): the last-block-in-turn mining-time cap was
baked into the shared delay computation, so the bid simulator's interrupt
decision and greedy-merge budget were squeezed to period/5 on every
last-in-turn block, discarding better late-arriving bids for no reason.
Add Parlia::delay_for_bid_simulation (no last-in-turn cap, no first-block
floor) and use it at both bid_simulator call sites; local mining keeps the
capped delay_for_mining. Fix 1 of that PR (tighter local cap) is already
covered by our period/5 cap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(miner): port go-bsc canBeInterrupted semantics and NoInterruptLeftOver default
The bid-preemption gate used a hardcoded 500ms threshold against the
leftover-subtracted, interval-clamped mining delay. With 450ms post-Fermi
slots that delay can never reach 500ms, so the interrupt-for-better-bid
branch was dead code, and the `delay_ms == 0` fallback committed fresh
simulations exactly when the (capped) budget claimed no time remained.
Mirror go-bsc instead:
- compare raw wall-clock time until the block target timestamp against the
threshold (`canBeInterrupted`), with no commit-at-zero arm — go-bsc
refuses zero-time simulations rather than starting them
- take the threshold from MiningConfig::no_interrupt_left_over (env
BSC_NO_INTERRUPT_LEFT_OVER) instead of a const
- derive the default via go-bsc's getDefaultNoInterruptLeftOver formula
with reth-bsc's 120ms finalize reserve substituted for go-bsc's 15ms:
110ms worst-case simulation + 10ms buffer + 120ms = 240ms (vs 135ms)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(miner): abort bid simulation when no time remains before sealing
Port go-bsc simBid's errNoTimeLeft guard: before doing any simulation
work, check delay_for_bid_simulation against the finalize reserve and
drop the bid if the window is gone. Reserve is our DELAY_LEFT_OVER
(120ms) rather than go-bsc's delayLeftOver (15ms), matching how
default_no_interrupt_left_over adapted the same constant.
Without this, a bid dispatched near the seal deadline — or one that went
stale while queued behind another run (the simulate loop is sequential) —
starts a simulation that cannot finish before sealing and delays viable
bids queued behind it. The aborted bid is removed from best_bid_to_run
(hash-matched, go-bsc DelBestBidToRun) so a committed-but-never-simulated
bid cannot block later admissions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(miner): port go-bsc simBid recommit and defer-style cleanup
Port the remaining go-bsc simBid mechanisms so an accepted bid can no
longer be silently dropped without ever being simulated:
- Recommit probe (go-bsc simBid defer): after a clean simulation, when no
new bids are queued, re-run the best simulated bid through admission.
If a better bid was parked non-committed in best_bid_to_run during the
run (no-interrupt window), the probe loses the expected-reward
comparison and the parked bid finally gets simulated — previously it
was only rescued if yet another bid happened to arrive. Otherwise the
probe re-dispatches the best bid itself (is_expected_better_than is >=,
as in go-bsc), deliberately re-running greedy merge over the current
mempool; the no-time-left guard terminates that loop at DELAY_LEFT_OVER
before the seal deadline.
- Defer-style cleanup: bid_simulate's simulating_bid removal, finished
flag, and DelBestBidToRun now run on every exit path (body extracted
into simulate_bid_inner returning Result). Previously the ~8 early
error returns leaked a phantom in-flight simulation that wrongly parked
incoming bids until clear() pruned it blocks later. DelBestBidToRun is
now hash-matched like go-bsc's, so an aborted bid cannot evict a newer
parked bid.
- Committed-flag fix: go-bsc shares one *types.Bid pointer between
bestBidToRun and the dispatched runtime, so Commit() marks both. Our
map held a clone inserted before commit_bid ran, leaving every
best_bid_to_run entry permanently non-committed: admission re-simulated
already-dispatched bids instead of discarding worse newcomers, and the
recommit probe would have looped forever. commit_bid now marks the map
copy (hash-matched) as well.
- Mid-run errNoTimeLeft check after committing bid txs, and recommit
admission (commit_bid_inner) bypasses the pending-bid dedup that would
reject an already-seen bid hash — go-bsc has no dedup on the newBidCh
path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* coinbase address cannot be used as contract address & use effective gas price instead to check for system tx
* test(bep675): guard 6-blob sidecar deserialize against stack overflow
Adds `blob_sidecar_deserialize_six_blobs_no_stack_overflow`, covering the
`BscBlobTransactionSidecar` Deserialize impl in isolation at the max blob
count (6 x 128 KiB) on a stack pinned to 2 MiB — the tokio worker default
that reth's RuntimeBuilder leaves unconfigured, and therefore the size the
`mev_sendBidBlock` decode actually runs on.
Complements the existing admission-path guard in
`node::miner::bid_block::tests::blob_admission_does_not_overflow_tokio_stack`
by asserting decode correctness as well as stack headroom:
- blobs/commitments/proofs are filled with a deterministic xorshift64
pattern (distinct seed per field and index) rather than left as
`Blob::default()`, so a decode that truncated, zero-filled, reordered or
transposed commitments/proofs cannot round-trip unnoticed
- `version: 1` rather than the `0` default, so a dropped field is visible
- asserts the serialized payload exceeds 6 * 2 * BYTES_PER_BLOB, so the
fixture cannot later be reduced to empty blobs and pass vacuously
- 20 iterations, with panics propagated via `resume_unwind` to preserve the
failing assertion's message and line (a genuine overflow aborts the
process instead, which is itself the signal)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(mev): instrument bid-simulation preemption for the thrash ratio
TC-019 gates the tightened MEV timing windows (`no_interrupt_left_over`
500ms→240ms, `bid_simulation_left_over` 50ms→20ms) on an interrupt-thrash
ratio, but nothing recorded interrupts at all: `can_be_interrupted` allowing a
preemption left only a `debug!` line, and `bid_simulation_duration_seconds` is
recorded after the early return for aborted runs, so preempted simulations were
invisible to every existing metric.
Adds four counters on `bsc.mev`, all in the same unit (simulations), nesting as
late ⊆ wasted ⊆ interrupt ⊆ started so every ratio is a true proportion:
- `bid_simulation_started_total` — counted beside the `simulating_bid` insert,
the sole gateway into `simulate_bid_inner`, so it includes aborted runs and is
the denominator TC-019's "占总模拟次数比例" is written against
- `bid_interrupt_total` — preemptions allowed, at the single decision point
- `bid_interrupt_wasted_total` — preempted simulations whose block then sealed
without any bid, for any reason
- `bid_interrupt_late_total` — the subset where a replacement was still in
flight when the bid was needed, i.e. the window itself was too tight. `wasted`
high with `late` low means bid quality, not the parameter change
Whether an interrupt was wasted is only knowable at the seal, so the simulator
keeps a per-parent tally (`InterruptTally`) between the two moments. It is
consumed by `take_interrupt_count` so a retried build cannot double-count, and
pruned in `clear()` alongside the bid maps for blocks that never reach the seal
path (a BidBlock win short-circuits earlier). Weighted by the tally rather than
1 per block so a six-deep cascade — the failure mode under test — does not read
like a single unlucky coin-flip.
`is_simulating` is sampled at bid-collection time rather than at the metric
site: `collect_payload_candidates` can block on `join_next` when no candidate
has landed, exactly the under-load case this measures, and a later read would be
biased toward reporting "on time".
Tally bookkeeping is split into free functions per the existing
`retain_recent_bid_blocks` convention, with four tests covering per-parent
accumulation, inclusive-threshold pruning, absent-parent-reads-zero, and
take-is-exactly-once.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* unit test error code send_bid
* fix(bep675): adopt bsc #3742 BidBlock signing hash + tx-root check (#455)
* test(bep675): pin BidBlock engine-error cleanup behavior (TC-004)
`on_new_bid_block`'s `Err(err)` arm — `engine.new_payload()` failing at the
transport level rather than returning a verdict — had no coverage. Adds
`bid_block_engine_err_leaves_double_sign_slot_claimed` asserting what it does
today: the block is still broadcast, the double-sign slot stays claimed, and the
builder keeps its SendBidBlock permission.
This pins correct behavior rather than recording a gap. The BidBlock is
announced to peers *before* verification (zero-simulate), so once the engine
errors this validator has already signed at that height. Releasing the slot
would let a fallback block be produced for the same height and turn a missed
slot into a slashable double sign; the rollback helper in `shared` is scoped to
blocks that were never broadcast (the miner's channel-send failure), which is a
different path. go-bsc agrees — `handleBidBlockResult` leaves
`recentMinedBlocks` untouched on a verification failure and has no rollback at
all.
Not revoking the builder is a deliberate divergence from go-bsc, noted in the
test: `InsertChain` returns a single error, so go-bsc cannot separate "block is
invalid" from "our node failed" and revokes on both. reth-bsc can, and only
revokes on `Ok(Invalid)`.
Verified non-vacuous: adding the slot release to that arm makes this test fail
with its stated reason. Two supports for that guarantee — an observation channel
on the engine mock so the test awaits proof `new_payload` was actually called
rather than inferring it from a timeout, and `EngineResponses`'s new
`unavailable_new_payload()`, which drops the responder so the handle yields
`Err(BeaconOnNewPayloadError::EngineUnavailable)` — the real shape of this
failure rather than a synthetic error value.
Test-only: all hunks are inside `mod tests`; production behavior is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(bep675): adopt bsc #3742 BidBlock signing hash + tx-root check
reth-bsc computes the BidBlock signing digest as `rlpHash([header, transactions,
sidecars])`. bsc changed it to `Header.Hash()` in 22387b2a6 ("miner: optimize
BidBlock signing hash", #3742) — shipped in v1.7.6 and v1.7.7, still current on
develop. So every builder on a current release is rejected by reth-bsc, closing
the BEP-675 route entirely.
The failure is silent rather than loud: ECDSA recovery over the wrong digest does
not error, it returns a different address. Admission therefore reports
"builder is not registered" for a correctly whitelisted builder, pointing at
whitelist config instead of the digest.
Verified against the local devnet with a single-variable A/B (same node, same
block, same whitelisted key, only the signed digest differing):
before: new digest -> "builder is not registered"; old digest -> accepted
after: new digest -> accepted and imported; old digest -> rejected
Two coupled halves, which must not be split:
1. `BidBlock::hash()` -> `self.header.hash_slow()`, verified byte-identical to
go-bsc `Header.Hash()`.
2. A `transactions_root` check in `verify_bid_block_payload` (go-bsc
`preSealVerifyBidBlock`'s `DeriveSha(decoded.Txs) == header.TxHash`), new
`PreSealVerifyError::TxRootMismatch` -> -38007.
Half 2 is what keeps half 1 safe. With the digest narrowed to the header, that
root is the only thing binding the submitted body to the signature — and
`simulate_bid_block` *overwrites* it with the root of whatever body it was
handed. Without the check, anyone could replay an honest builder's (header,
signature) with a substituted transaction list, have it proposed under that
builder's identity, and leave no trace; the honest builder would absorb the
revocation.
The root must come from the RAW submitted bytes, not from re-encoding the decoded
transactions. Parlia's trailing system txs arrive unsigned with V=R=S=0, but
reth's `Signature` stores only a parity bit, so re-encoding a legacy tx emits
v=27 and yields a different root than go-bsc's DeriveSha over the same input.
An earlier revision of this fix did re-encode and rejected every legitimate bid
on the devnet; the unit fixtures could not catch it because both sides used the
same function and so agreed with each other while disagreeing with geth. Pinned
now by a cross-client vector that also asserts the re-encoded root differs.
Test vectors are regenerated from current bsc source rather than edited by hand.
Three of them invert rather than just changing value, because the digest is now a
pure function of the header: vector_b must equal its own header hash, and the
blob-sidecar vector must equal vector_a exactly. The four old vectors were green
throughout — they pinned the stale formula, which is why nothing caught this.
Sidecars are no longer covered by the signature; they are bound through the blob
versioned hashes carried inside the (root-committed) transactions. That chain is
not audited here — tracked separately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(bep675): make the ef-tests BidBlock harness commit to its own body
`execution_gate_round_trip` reused the reference block's header while replacing
its signed deposit with an unsigned placeholder in the submitted body. The
header's `transactions_root` therefore committed to the signed deposit while the
body carried the unsigned one — a BidBlock no real builder could produce, and one
the new tx-root check correctly rejects.
Recompute the root over the bytes actually submitted, which is what a builder
does now that the signature covers only the header. `simulate_bid_block` still
overwrites the root with the re-signed set, so the sealed header and the state
root this test compares are unaffected.
Caught by CI (`cargo test --all`), not by the lib suite: this harness lives in the
`bsc-ef-tests` workspace member.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(bep675): re-root debug_buildCandidateBlock over the body it returns
The shadow-builder seam shipped the *built* block's header, whose
`transactions_root` commits to the signed system txs, alongside a body that
re-emits those system txs unsigned (`v=r=s=0`) as its own docstring describes. The
header therefore did not commit to its own body — a candidate no real builder can
produce, and one `verify_bid_block_payload` now rejects with `invalid tx root`
since the signature covers only the header (bsc #3742).
Found on the devnet: every blob/special/tier2 case failed at admission with
err=invalid tx root: got 0xc3b75297…, want 0x77b73b00…
Recompute the root over the transactions actually returned. Safe for what this
seam exists to exercise: `simulate_bid_block` recomputes the root after
bind-signing, so the sealed header and its state root are unaffected.
Third instance of the same latent defect, all in our own tooling and all
previously invisible because nothing checked that a BidBlock header committed to
its body: the ef-tests harness reusing a reference header, this endpoint shipping
the sealed root, and an earlier revision of the check itself re-encoding decoded
txs instead of hashing the submitted bytes.
Post-fix devnet run: tier1 15/15, quota 2/2, tier2 3/3 (bids canonical with
correct state roots, double-sign guard and post-import revocation both firing).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): point reth pins at develop b195a7318
Move all 57 reth-* pins (42 root, 15 bsc-ef-tests) from c13b0986 to
b195a7318, so the tree follows bnb-chain/reth develop rather than main.
No code change: c13b0986 (main head) and b195a7318 (develop head) have
the same tree SHA f68ffc2a. The branches diverged only in history --
the receipt-streaming fix (#206) landed on develop as 8555c25c and on
main as #207 -- so their contents are byte-identical today.
Note: reth-metrics resolves twice in Cargo.lock, because rust-eth-triedb
(pulled in transitively by upstream reth) declares it off bnb-chain/reth
with no rev, which Cargo resolves to main's head. Pre-existing, and
harmless while the branches agree. Cannot be fixed with [patch] here --
Cargo rejects patching a source with itself; the fix is a rev on that
line upstream in bnb-chain/reth-bsc-triedb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* hf dates for qa net
* fix(qanet): defer Pasteur pending state-root commit bug
Pasteur activated on bsc-qanet at 1785897000 (2026-08-05 02:30 UTC) and
immediately split the cluster: geth v1.7.7 rejected block 21323714 with
`invalid merkle root` (remote c40b3aae..., local 47371e77...).
The system-contract upgrade writes the new StakeHub (0x2002) and Governor
(0x2004) code to plain state -- `eth_getCode` at 21323714 returns the new
94152/56974-byte code -- but the changed codeHash is not committed to the
trie, so the header's state root describes the un-upgraded state. Confirmed
by deferring Pasteur on geth: with the upgrade not applied it computes
exactly c40b3aae and follows the chain. All three reth validators share the
defect, so they agree and the chain advances; geth is orphaned.
Defer Pasteur to 1790000000 so the cluster runs post-Osaka/Mendel and geth
can follow, and re-arm it once the trie commit is fixed. Osaka and Mendel
stay at 1785897000 -- geth accepted 21323714..21327713 with only Pasteur
deferred, so both are compatible. This value must stay equal to geth's
`--override.pasteur` on the QA cluster.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(evm): report direct state writes to the state hook
Four sites committed state straight to the DB without announcing it to the
state hook, so the incremental state-root pipeline (sparse trie /
`StateRootTask`) never saw the change and the block committed a root
describing the pre-write state:
executor.rs upgrade_system_contract contract code -> codeHash
executor.rs apply_history_storage_account EIP-2935 code + nonce
post_execution.rs distribute_incoming zero SYSTEM_ADDRESS balance
post_execution.rs distribute_incoming credit validator reward
`commit_transaction` already had the correct shape - `system_caller.on_state()`
before `db.commit()` - so all four now follow it, with the upgrade source
threaded through `upgrade_contracts` (PreBlock before Feynman, PostBlock after).
This split bsc-qanet at the Pasteur transition: reth sealed block 21323714 with
root c40b3aae (StakeHub/Governor un-upgraded) while `eth_getCode` at the same
block returned the upgraded 94152/56974-byte code. geth computed 47371e77 and
rejected. Deferring Pasteur on geth made it compute c40b3aae and follow,
confirming reth's root omitted the code writes.
The two balance sites fire on every block with block_reward > 0, so this is
not limited to fork transitions; qanet only exposed the rarest of the four
because its empty blocks make distribute_incoming early-return.
Verified on node-deploy-bsc (3 reth + 1 geth, chain 714, Pasteur ~60s after
genesis), same topology as the QA cluster:
unfixed: geth froze at 15 with BAD BLOCK 16 "invalid merkle root"
(miner 0x5e2A531A..., the same validator as on qanet)
fixed: all four nodes lockstep to block 486, zero bad blocks,
TC-008 7/7 incl. identical hash+stateRoot at transition block…
Removes all triedb-related code paths, keeping only the MDBX-based state backend. Eliminates the conditional
is_triedb_active()branching across the engine, execution, storage, RPC, and node builder layers.🤖 Generated with Claude Code