中文版(中文): bsc-diff.zh.md
Analysis subjects:
- Go
bnb-chain/bsc@c5533ab5b(binary reportsv1.8.0-alpha; built 2026-10-11)- Rust
bnb-chain/reth-bsc@98ef6c5(v0.1.2line; built 2026-10-11)Conclusions rest on three pillars: source-code reading, live private-network experiments (Go-only, Rust-only, and mixed networks; see privnet.md and
privnet/), and live web research verified on 2026-10-11 (release pages, BSC docs announcements, validator docs).
A Rust node is a usable read-data full node / RPC source and can even produce blocks on a private network (verified), but it is not proven as a mainnet validator; for anything finality-critical, trust Go or cross-validate the two clients (see the probe crate below).
| Question | Answer |
|---|---|
| Are both implementations "fine / production-grade"? | No. Go is a battle-tested production client. Rust is officially experimental / under active development; its block-production code exists and works on private networks (verified, §3), but it is not proven in the mainnet validator set. |
| Can they sync with each other? | Yes — verified live. Private-network tests ran Go-only, Rust-only, and mixed (2+2) topologies to identical hashes/stateRoots; both miner-handover directions work (see privnet.md). The devp2p wire protocol, plus the bsc/1 and bsc/2 sub-protocol message codes and RLP encoding, are byte-for-byte identical; Rust has interop tests against Go's reference encoding. |
| Are they used heavily in production? | Go is the client actually run by BSC mainnet's 45 active validators (21-strong consensus set). Rust currently serves full-node / archive-node / RPC roles. A Rust node someone gives you ≈ a read/RPC node. |
| Are RPC / P2P different? | P2P is near-fully compatible (Rust mirrors Go's wire format; measured handshake matrix in privnet.md). RPC namespaces mostly align, but Rust is missing some BSC-specific finality APIs, and its handling of the safe/finalized tags differs semantically from Go. |
- go-ethereum fork, BSC's original / authoritative implementation (
bsc/README.md:1-13). - Full PoSA/Parlia block-production capability:
Prepare/Finalize/FinalizeAndAssemble/Sealare all implemented (bsc/consensus/parlia/parlia.go:1154,1401,1510,1723) — it can validate and produce blocks. - Mature release process: Stable / Feature / Preview channels (
bsc/README.md:23-28). Repo HEAD isv1.8.0-alpha(a preview tracking geth v1.17.3,bsc/CHANGELOG.md:3). - Full toolchain: clef, devp2p, abigen, bootnode, evm; complete hardware docs (3TB SSD / 16C / 64G,
bsc/README.md:105-117). - Release posture (verified live 2026-10-11): latest tag
v1.8.0-alphais an explicit preview, not for production; latest production line isv1.7.8.v1.8.0-alphatracks go-ethereumv1.17.3. Theauthrpc.*flags are compiled out (cmd/geth/main.go) — BSC embeds consensus, there is no beacon Engine API (by design).
- Not a fork of reth, but an extension built on reth's
NodeBuilderAPI (reth-bsc/README.md:3,reth-bsc/CLAUDE.md:7). All BSC-specific logic (Parlia, system contracts, PoSA mining, MEV, EVN, precompiles) lives in this repo. - Official status table (
reth-bsc/README.md:9-19):- Historical Sync ✅, BSC Pectra Support ✅, Live Sync ✅
- Run as validator ❌ (soon) ← still printed, but stale: validator-enabling releases shipped (
v0.0.6-beta, 2025-12-10; latestv0.1.2, 2026-08-20 with BEP-675mev_sendBidBlock). No public evidence of mainnet validators running it; the disclaimer ("experimental… use at your own risk") still stands.
- At the code level, PoSA mining is already written (
reth-bsc/src/node/miner/,reth-bsc/MINING.md,reth-bsc/CLAUDE.md:75describes theBscMiner::startproduction loop), but its production-readiness is not yet endorsed — the code exists, but it has not been production-validated / certified as a validator. - Private-network proof (this repo, privnet.md): a Rust node mines from genesis, follows over
eth/69, produces blocks Go accepts (system transactions included), and hands the producer role both ways with Go. Private-net testing found and fixed the blockers (zero-hash initial Status deadlock; system-contract decode panic — the latter is fixed upstream ondeveloptoo; a review-driven hardening gates custom-chain handling on genesis identity for reused chain ids). Fixes live onakfork:fix/fix-privnet, PR bnb-chain/reth-bsc#546; all modes re-verified on both clients'developbranches (§10).
Maturity verdict: Go is an order of magnitude ahead and is the "only production-validated client". Rust is usable for "full-node / archive-node sync + read RPC" and now demonstrably mines on private networks; mainnet validation remains unproven.
They sync — verified live on private networks (privnet.md): Go-only, Rust-only (from genesis), and mixed 2+2 topologies converge to identical hashes/stateRoots; user transactions propagate and execute identically in both directions; block production hands over both ways.
- Hard-fork timelines match on both sides: Ramanujan → Niels → MirrorSync → Bruno → Euler → Gibbs → Nano → Moran → Planck → Luban → Plato → Hertz → Kepler → Feynman → FeynmanFix → HaberFix → Bohr → Pascal → Prague (Pectra) → Lorentz → Maxwell → Fermi → Osaka / Mendel → Pasteur → Jenner (qanet-only) (all activated on schedule: Fermi mainnet 2026-01-14, Osaka/Mendel 2026-04-28, Pasteur 2026-08-25, Jenner qanet 2026-10-05; mainnet/testnet do not schedule Jenner yet).
- Go:
bsc/params/config.go,bsc/core/systemcontracts/{feynman,planck,luban}/* - Rust:
reth-bsc/src/hardforks/bsc.rs,reth-bsc/src/system_contracts/(CLAUDE.md:77 lists the same list) - Go:
bsc/params/config.go,bsc/core/systemcontracts/{feynman,planck,luban}/* - Rust:
reth-bsc/src/hardforks/bsc.rs,reth-bsc/src/system_contracts/(CLAUDE.md:77 lists the same list)
- Go:
- Both consensus engines are Parlia (PoSA); both maintain a validator-set snapshot at epoch boundaries (
snapshot.rs) and implement BLS voting (vote_pool/bls_signer). Rust deliberately re-implements Go's RNG (go_rng.rs) to keep block-ordering deterministic. - Both validate EVM equivalence via execution-spec-tests (EEST) (Rust
make ef-tests; Go has its own long-standing tests), reducing the risk of "same tx, different state root across clients".
- Go offers
eth/{70, 68}(v1.8.0-alphamadeeth/70primary but explicitly "keeps eth/68 for compatibility",bsc/CHANGELOG.md:19;eth/70carries noUpgradeStatusMsg,bsc/eth/protocols/eth/handshake.go). - The Rust reth fork advertises
eth/{69, 68, 67, 66}(LATEST = Eth69). - Measured negotiation (private-net test): Rust↔Go settles on
eth/68(Status +UpgradeStatusround-trip on both sides — fully compatible); Rust↔Rust settles oneth/69. No incompatibility observed; Go has announced noeth/68removal date, so the "must upgrade to eth/70" point is to track, not an active gap.
Both sides align field-by-field (bsc/eth/protocols/bsc/protocol.go vs reth-bsc/src/node/network/bsc_protocol/protocol/proto.rs):
| Item | Go | Rust |
|---|---|---|
| Protocol name | "bsc" |
"bsc" |
| Versions | bsc/1, bsc/2 |
bsc/1, bsc/2 |
| Message count | v1=2, v2=4 | v1=2, v2=4 |
0x00 BscCapMsg |
✅ | ✅ (Capability) |
0x01 VotesMsg |
✅ | ✅ (Votes) |
0x02 GetBlocksByRange |
✅ | ✅ |
0x03 BlocksByRange |
✅ | ✅ |
Rust has a unit test using Go's RLP encoding as the reference vector (proto.rs:64 test_bsc1_votes_messages_rlp_matches_reference, against real captured Go-node bytes f90973…), proving wire-format equivalence. Both implement BSC's GetBlocksByRange (max 64 blocks per request, 8MB soft cap) and a total-difficulty-threshold-based vote-broadcast strategy (deltaTdThreshold=1000; Rust registry.rs:309 comment "Matches go-bsc eth/handler.go").
- BSC adds an extension message during the eth handshake to tell the peer "whether to disable transaction broadcast" (Enhanced Validator Network).
- Rust's
upgrade_status.rsdecodes bytes captured from a real BSC node (0bc180/0bc2c101, message id0x0b), matching Go'sUpgradeStatusMsg(bsc/eth/protocols/eth/handshake.go:88,103). - Note: Rust's
UpgradeStatusExtension.disable_peer_tx_broadcastis markedTODO: support(upgrade_status.rs:56): the flag is sent (gated on EVN readiness,handshake.rs:32-36) and decoded from real BSC captures, but the receive side only logs (handshake.rs:82-85, peer id still TODO). Go already enforces it. Rust separately implements EVN via an independent--evn.enabledflag (activated only after sync completes and head lag < 30s, CLAUDE.md:91) — a different path to the same goal.
P2P takeaway: wire protocol is interoperable (verified live, not just by code reading); Rust is the follower/aligner. Negotiation lands on eth/68 against Go, eth/69 Rust-to-Rust.
| Namespace | Go (bsc) |
Rust (reth-bsc) |
Notes |
|---|---|---|---|
eth |
✅ full | ✅ full | standard Ethereum API + BSC extensions |
net / web3 |
✅ | ✅ | |
admin |
✅ | ✅ (rpc/admin.rs) |
|
debug |
✅ rich | ✅ (rpc/debug_builder.rs) |
|
txpool |
✅ | ✅ | |
trace |
Go debug_trace* (+ tracers) |
develop adds OpenEthereum-style traceBlock* under debug (wired, not exercised here); main only lists it |
|
parlia |
✅ getSnapshot/getValidators/getJustifiedNumber/getFinalizedNumber/... |
✅ same name/params (rpc/parlia.rs, plus buildAddNodeIDsCall/RemoveNodeIDsCall) |
Rust output matches bsc-erigon/geth shape |
miner |
✅ (geth built-in + BSC miner_mev*/addBuilder) |
✅ redefined (rpc/miner.rs, covers miner_setExtra/setGasPrice/setGasLimit, drops reth's unimplemented defaults) |
|
mev |
✅ (internal/ethapi/api_mev.go) |
✅ (rpc/mev.rs, bid submission + MEV API; mev_sendBidBlock since v0.1.2) |
|
eth_createAccessList |
✅ | develop, rejects pre-London types on chains without London (observed) |
|
blob |
via eth_getBlobSidecars etc. |
✅ (rpc/blob.rs reads blobs from pool/provider) |
|
eth_health / eth_coinbase |
Go has eth_health; geth provides eth_coinbase |
✅ eth_ext.rs explicitly implements eth_coinbase + eth_health |
Rust removes reth's default empty impl first, then registers (main.rs:499-500) |
Confirmed by code search and live-API checks: Rust has none of the following Go-documented methods (no match in reth-bsc/src):
eth_getFinalizedHeadereth_getFinalizedBlocketh_newFinalizedHeaderFiltereth_getHeaderByNumbersupport for the"finalized"tageth_getTransactionsByBlockNumber(server:bsc/internal/ethapi/api.go:2060)eth_getTransactionDataAndReceipt(server:bsc/internal/ethapi/api.go:2228)
(eth_getBlobSidecars / eth_getBlobSidecarByTxHash do exist on both sides.)
Impact: any workflow that depends on BSC finality proofs (cross-chain bridges, IBC / light clients, relayers) cannot get a finalized head/block from a Rust node — it must use Go.
parlia_getSnapshotetc.: Go uses*rpc.BlockNumber(supportssafe/finalizedtags); Rust took a string and treated bothsafeandfinalizedaslatest(parlia.rs:195-204onmain). Ondevelopthis is fixed:safe/finalizedresolve through the newrpc/finality.rsfrom the provider's real canonical safe/finalized pointers (verified live:parlia_getValidators/getFinalizedNumberanswer; pre-Luban chains report 0, as there are no votes yet).parlia_getFinalizedNumber: Go returnsAttestation.SourceNumber(api.go:114); Rust returnssnapshot.get_finalized_number()(parlia.rs:405) — assuming the snapshot structure is identical, the two should be equivalent, but the tag-handling difference changes the input block, hence the result.
| Capability | Go bsc |
Rust reth-bsc |
Notes |
|---|---|---|---|
| Full-node sync | ✅ production | ✅ (synced to mainnet tip; README cites ~10.6T disk) | |
| Archive node | ✅ | ✅ | |
| Snapshot sync | ✅ mature (snap/1) |
❌ no snap protocol (DB-snapshot bootstrap + staged/live sync instead) | corrected: earlier doc implied parity |
| Block production / validator | ✅ production validator | ✅ works on private nets (verified, privnet.md); not proven on mainnet, README still marks ❌ | was "code exists but not endorsed" — now tested |
| EVM equivalence | ✅ | ✅ (runs EEST) | |
| System contracts / precompiles (BLS, CometBFT light client, IAVL…) | ✅ | ✅ (src/evm/precompiles/*) |
|
| MEV / bid | ✅ production | ✅ (mev_sendBidBlock since v0.1.2) |
|
| BLS voting / double-sign detection | ✅ | ✅ | |
| EVN (validator network) | ✅ | ||
| Light node (LES) | ❌ (no les/ in tree) |
❌ reth has no LES | corrected: neither supports light sync |
| Metrics | ✅ :6060/metrics |
✅ --metrics 0.0.0.0:6060/metrics |
- Who runs in production: BSC mainnet's 45 active validators (21-strong consensus set) run Go
geth-bsc. Rust nodes appear in read-only / infrastructure roles, not in block production; no public evidence of mainnet validators running Rust. - Risk of using a Rust node as an RPC source: it is a young implementation; even though EVM equivalence is covered by EEST, extreme edge-case consensus divergence is still possible → it may return blocks / state inconsistent with Go; it lacks the finalized API series; and the official disclaimer says "use at your own risk".
Private-net testing additionally found and fixed two consensus-adjacent bugs (Status-head deadlock, epoch-boundary decode panic — the latter is fixed upstream on
developtoo) — fixed code paths should be re-verified on mainnet data before trusting them there. - Risk of using Rust as a validator: the code mines correctly on private networks (verified), but mainnet validation is unproven and the project still marks it ❌.
- Verified interop points:
bsc/1,bsc/2message codes and RLP are byte-for-byte identical (Rust has a real captured-byte comparison test);eth/68interconnects against Go andeth/69Rust-to-Rust (measured live, privnet.md); same-height hashes/stateRoots match across clients including user transactions.
Your architecture: you use revm in your own process to sync BSC block state and run simulations. This means:
- The client is only your state and block data provider; it does not need to run revm locally (you do).
- Key needs: accurate block headers (especially
stateRoot), accurate txs + receipts, and arbitrary-height historical state (for replay / simulation). - Finality awareness (your bundle must target a "safe" head and avoid blocks that may reorg) requires a finalized/head concept.
| Use | Recommended client | Why |
|---|---|---|
| Primary state source / authoritative read | Go archive node | production-validated, full finality API, trustworthy state root |
| Historical-state replay/sim (feed revm state) | Go archive (primary) + Rust archive (cross-check) | history needs archive; cross-checking state roots cuts chain-split risk |
| Chain head / finality awareness (pick bundle target) | Go | Rust lacks eth_getFinalizedHeader and treats finalized as latest |
| Bulk reads / offload Go | Rust full/archive node (redundancy/compare source only) | read RPC works, but run the probe for state-root consistency before every production use |
eth_call / eth_getStorageAt / eth_getProof (sim inputs) |
Go (preferred) | result equivalence needs probe verification; Rust is redundancy |
eth_getBlockByNumber(full) / eth_getBlockReceipts (build revm block) |
either, but must be compared | matching header/tx-root/receipt-root is the precondition for "same world state" |
- World-state consistency must be guaranteed before revm simulation: use
eth_getBlockByNumber(height, true)to fetch the block header (stateRoot) and txs,eth_getBlockReceiptsfor receipts, andeth_getProof/eth_getStorageAtto pull account + storage to fill revm's state DB. ThestateRootof the two clients at the same height must be equal — this is exactly what thebsc-consistency-probecrate does. - Historical simulation needs an archive node: a full node keeps only recent state and cannot serve old-block state. Both Go and Rust archive modes support reads (
bsc/README.md:75,reth-bsc/README.md:75), but Rust archive's "read consistency" must be measured with the probe. - Do not trust Rust's
safe/finalized: if your searcher useseth_getBlockByNumber("finalized")to pick a safe height, Rust returnslatest— you may build on a head that is not yet finalized. Always go through Go. - Dual-client cross-check is cheap insurance: run the probe regularly at critical heights (especially production peaks, around hard forks); on any state-root mismatch, alert immediately and fall back to Go.
The companion tool is the Rust crate
bin/bsc-consistency-probein this repo: it cross-validates same-height block headers (includingstateRoot/receiptsRoot/transactionsRoot) across a Go endpoint and a Rust endpoint, and additionally has atruthmode that verifieseth_getProofMerkle proofs against the reported state root.
- The two implementations are not "both fine / both production-grade": Go is production-grade and produces blocks; Rust is experimental but now demonstrably mines and syncs on private networks — mainnet validation remains unproven.
- They sync with each other — verified live: same underlying chain, consensus, and hard forks; the P2P wire protocol (including BSC's custom sub-protocol) is strictly mirrored by Rust, covered by captured-byte tests and by end-to-end private-network runs (Go-only, Rust-only, mixed, miner handovers both ways).
- RPC mostly aligns, but Rust is missing BSC's finalized/finality APIs (plus
eth_getTransactionsByBlockNumber/eth_getTransactionDataAndReceipt) and handlessafe/finalizedtags differently from Go — for finality-sensitive work, use Go. - A Rust node someone gives you = a usable read / RPC full node (and a workable private-net validator), good for redundancy and comparison, not fit as a mainnet authoritative source or mainnet validator.
- For your MEV / DEX-aggregator scenario: Go archive node as primary state source, Rust archive as redundancy/compare source, run the probe for state-root consistency before every production use; finalized/head decisions go to Go only.
- To reproduce all of the above:
privnet/one-script private networks; full process and results in privnet.md.