Skip to content

miner: persist BidBlock revokes across restarts - #3796

Merged
joey0612 merged 3 commits into
bnb-chain:developfrom
joey0612:fix/bidblock-revoke-persist
Aug 31, 2026
Merged

joey0612 merged 3 commits into
bnb-chain:developfrom
joey0612:fix/bidblock-revoke-persist

Conversation

@joey0612

Copy link
Copy Markdown
Collaborator

Description

BidBlock SendBidBlock revokes (BEP-675 Layer 2) were held only in an in-memory map in BidBlockPermissionManager, reconstructed empty on every miner.New. A validator that revoked a builder for 24h and then restarted within that window (upgrade, crash, OOM) came back with an empty map, so the builder was immediately allowed to SendBidBlock again — even though the permission RPC still advertised resetAt = revokedAt + duration as a wall-clock promise.

This PR persists the revoke map to the chain database and restores it on startup:

  • The whole map is mirrored to a single database key (bidBlockRevokes), overwritten on every change. It is JSON-encoded — RLP is geth's codec for consensus-critical / large / frequent data, but supports neither maps nor time.Time; this state is tiny, written rarely, never on-chain, and benefits from JSON's schema tolerance and readability, matching the existing WriteChainConfig precedent.
  • On startup the manager loads the persisted map and keeps only revokes still within their window, dropping any whose lockout elapsed while the node was offline. So a revoke survives a restart within its window; one that fully expired during downtime is not resurrected.
  • Persistence is asynchronous and best-effort: mutations snapshot the map under the lock and hand it to a fire-and-forget writer, so the mining path never blocks on disk I/O. Losing the newest write on a crash is acceptable; what must not happen is a disk hiccup stalling Revoke/SetAllowed.
  • Writers are serialized and carry a monotonic sequence number. The seq guard is advanced before the write and regardless of its outcome, so the seq that actually reaches db.Put is strictly increasing: once a newer snapshot has entered the writer, an older one is dropped even if the newer write failed — an older snapshot can never overwrite a newer one and resurrect stale state (e.g. a revoke overwriting a later manual clear).

The manager keeps a nil-database path (unchanged in-memory behavior) for callers and tests that don't wire a database.

Rationale

A revoke that silently vanishes on restart lets a misbehaving builder resume SendBidBlock (bypassing the weaker SendBid fallback) before its lockout should have ended, and makes the RPC's resetAt a promise the node doesn't keep. Durability of the lockout across restarts is what the advertised 24h window requires.

Scope is deliberately limited to durability across restarts: revoke triggers, the 24h / 450s durations, the RPC semantics and the per-validator (non-shared) nature of the lockout are all unchanged.

Example

NA

Changes

Notable changes:
NA

@hashdit-bot

hashdit-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

Pull Request Review

This PR adds asynchronous, best-effort persistence of BidBlock builder revocations to the chain database and restores active revocations when the miner restarts. It also introduces sequence-based write ordering, updates miner backends to expose the chain database, and adds regression tests for restart durability, expiration, corrupt state, failed writes, and manual clears.

Sensitive Content

No sensitive content detected.

Security Issues

No serious security issues detected.


Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits.

// a later manual clear). Losing the newest write on failure is acceptable
// (best-effort); an older snapshot overwriting a newer one is not.
func (m *BidBlockPermissionManager) persistAsync(seq uint64, snapshot map[common.Address]BidBlockRevokeRecord) {
m.persistMu.Lock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider following the VoteJournal approach by persisting the JSON under a dedicated /bidBlockRevokeJournal/ directory instead of chaindata, since BidBlock revocations are validator-local MEV policy rather than chain data: https://github.com/bnb-chain/bsc/blob/master/core/vote/vote_journal.go.

@joey0612 joey0612 Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — moved it out of chaindata. Revokes now journal to a single atomic JSON file under the datadir (/bidblockrevokes.json, path resolved via stack.ResolvePath like the txpool journal), and the ChainDb() addition to miner.Backend is gone. I kept it a snapshot file rather than a VoteJournal-style WAL dir: the WAL fits append-heavy per-block votes, while this is a rarely-mutated small map with self-expiring (≤24h) entries — a whole-map atomic replace is simpler and crash-safe (temp file + rename). Same restore-on-restart semantics and ordering guarantees, now with file-based tests.

@hashdit-bot

hashdit-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

Pull Request Review

This Go-based Ethereum client PR adds file-backed JSON journaling for BidBlock builder revocations, resolving the journal path under the node datadir and restoring active lockouts at startup. Mutations are persisted asynchronously using atomic replacement and sequence-based stale-write suppression, with configuration defaults and regression tests covering restart recovery, expiry, corrupt state, ordering, and manual clears.

Sensitive Content

No sensitive content detected.

Security Issues

No serious security issues detected.


Generated by Hashdit Bot. This tool can absolutely NOT replace manual audits.

will-2012
will-2012 previously approved these changes Aug 27, 2026
// expired ones are dropped.
func NewBidBlockPermissionManager(journalPath string) *BidBlockPermissionManager {
m := &BidBlockPermissionManager{
revoked: make(map[common.Address]BidBlockRevokeRecord),

@flywukong flywukong Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding explicit json: tags and a version envelope to the journal.

. The journal may change format (may need to carry a per-builder revoke count as well to support new feature like dynamic reovke time computiing) , so the lockout duration can scale with the number of offenses — adding fields for that is then a clean v1→v2 migration rather than a compat branch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@joey0612
joey0612 merged commit af7360c into bnb-chain:develop Aug 31, 2026
8 checks passed
joey0612 added a commit to bnb-chain/reth-bsc that referenced this pull request Sep 3, 2026
Port of bnb-chain/bsc#3796. BidBlock revoke lockouts (BEP-675,
validator-local MEV policy) previously lived only in memory, so a
restart silently released every revoked builder. They are now mirrored
to a JSON journal — a single standalone file under the datadir
(bidblockrevokes.json), deliberately outside chaindata — and restored
at startup, dropping any revoke whose window elapsed while the node
was down.

Semantics (matching go-bsc):
- Versioned envelope ({version: 1, revokes: {...}}) with pinned JSON
  keys (revokedAt/duration/reason/blockHash/blockNum); an unknown
  version, corrupt file, or missing file starts empty rather than
  misreading the format.
- Persistence is asynchronous and best-effort: mutations snapshot the
  whole map under the write lock and hand it to a fire-and-forget
  writer, so the mining path never waits on disk.
- The write is atomic (temp file + rename, owner-only permissions) and
  the persisted sequence advances before the write regardless of
  outcome, so an older snapshot can never overwrite a newer one and
  resurrect stale state (e.g. a revoke clobbering a later manual
  clear).
- Manual clears (mev_setBuilderBlockPermission allowed=true) persist
  too, so they are not resurrected on restart.
- BSC_BID_BLOCK_REVOKES_JOURNAL overrides the location (a relative
  path resolves under the datadir); an empty value disables
  persistence — the go BidBlockRevokesJournal config equivalent.

The journal path is registered during component build (payload service
builder, the miner bootstrap point), before any consumer initializes
the lazily-created global manager. Managers constructed without a
journal (tests, fallback) keep the previous purely in-memory behavior.

Tests mirror the go additions: restart survival with an identical
status snapshot, expired-while-down revokes dropped on load, corrupt/
missing journal tolerance, unknown-version refusal, stable-key pinning,
failed-newer-write-blocks-older ordering, and manual-clear persistence.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants