コンテンツにスキップ

BNB Smart Chain

UMNode speaks to BNB Smart Chain (BSC) — mainnet and the Chapel testnet — as --network bnb-mainnet and --network bnb-testnet. BSC is an EVM chain run by the bsc client, a go-ethereum fork with its own Parlia proof-of-staked-authority consensus. It is not an OP-stack chain: it needs no second layer and no derivation, but it differs from Ethereum at the wire handshake, in the fork schedule, and — most of all — in how a block's state is computed. This page lists what UMNode does about each, and what it does not (yet) do.

./bin/umnode --network bnb-mainnet \
  --block-db data/bnb/chain --state-db data/bnb/state \
  --node-db data/bnb/p2p/enode --inventory-db data/bnb/p2p/inventory

Every store is bound to the genesis it was created with, so give the BNB networks their own directories, as for any other network.

Where the chain description comes from

The YAML entries carry only the bootnodes and GenesisFile; everything else is derived from the bundled genesis files, exactly as for Ephemery (Configuration):

bnb-mainnet bnb-testnet (Chapel)
Chain id 56 97
Genesis 0x0d21840a…57b5b 0x6d3c66c5…fbe34
Genesis file cfg/bnb/mainnet.json cfg/bnb/testnet.json
Bootnodes bsc params/bootnodes.go bsc's release testnet/config.toml

The genesis files are bsc's release genesis.json (allocation and header fields, v1.7.8) with the full fork schedule from bsc's params/config.go — through Pasteur (August 2026) at the time of writing. bsc's own release file lists only the genesis-time forks and fills in the rest from code, so the bundled file is what bsc actually runs; keep it current when BSC schedules a fork.

Consensus: parlia marks a BNB network. It is set automatically when the genesis file's chain config carries a parlia engine section, and may be set explicitly for a Parlia chain run without a genesis file (blocks only).

Fork id

bsc computes the EIP-2124 fork id over every activation in its chain config, its own forks included (Ramanujan, Niels, …, Hertz, Kepler, …, Pasteur). go-ethereum's chain config has no fields for those and silently drops them, so UMNode derives the schedule from the raw config of the genesis file with the same folding rules bsc uses (bnb.Config.ForkSchedule). The result is pinned against the fork ids bsc's own core/forkid computes, at every activation, in bnb/testdata/bsc-forkid.json.

Total difficulty

Parlia never merged, and bsc still exchanges total difficulty in its handshakes. A BNB network advertises the genesis block's difficulty (1) — what a freshly started bsc node does — unless TotalDifficulty is set.

Networking

bsc negotiates eth under the same capability name as Ethereum, but its two versions differ from upstream's at the handshake, so a node must advertise one family or the other. On a Parlia network UMNode advertises eth/70 and eth/68 (bsc has no eth/69) and handshakes bsc's way:

  • eth/70 — bsc's status keeps the total difficulty that upstream eth/70 dropped: [version, networkid, td, genesis, forkid, earliest, latest, latestHash]. Block ranges, headers, bodies and the eth/70 receipts messages are otherwise upstream's.
  • eth/68 — after the standard status exchange, bsc sends an UpgradeStatus message (code 0x0b) each way and drops a peer that does not answer. UMNode answers, and uses it to tell the peer it needs no transaction broadcasts (it has no pool).

bsc's bsc/1–bsc/2 extension protocol (votes, finality) is optional on the bsc side and not negotiated. Block bodies may carry a fourth element — the blob sidecars bsc keeps for recent blocks — which the body decoder already ignores (it validates transactions, uncles and withdrawals against the header). Receipts are fetched over eth/70 and from RPC peers; an eth/68 peer's legacy receipts format is not requested.

Everything else — discovery (v4), the head poller, the fetcher's validation, the realtime and backfill workers, the block store — works unchanged. BSC produces a block every 0.45 s (since Fermi) and keeps ~18 days of blob sidecars; the default --seal-confirmations of 256 (about two minutes) is well past Parlia's finality.

Execution

A BSC block is not a go-ethereum block with a different fork schedule. The bnb package carries what Parlia adds, and the executor (evm package) applies it at the same points bsc's StateProcessor.Process and Parlia.Finalize do:

Parlia rule What UMNode does
Fees go to the system address 0xff…fe, not the coinbase; blob fees too, where Ethereum burns them The state's fee credit is redirected (StateDB.SetFeeSink); blob fees are credited after each blob transaction
System transactions — the validator's zero-priced calls to the system contracts at the end of every block (deposit, slash, distributeFinalityReward, updateValidatorSetV2, the Feynman initializes, block 1's inits) Recognised like bsc does (bnb.IsSystemTransaction) and applied with bsc's rules: no intrinsic gas, no refund, no fee, gas counted outside the block gas pool, the access list cleared (pre-Cancun) or prepared (Cancun+), nonce advanced, receipt always successful
The fee sweep — before generating the deposit (and, pre-Kepler, the 1/16 share to the system reward contract), bsc moves the accrued fees from the system address to the validator, whose transaction then carries them Performed before the corresponding system transaction the block carries; the amount must match, and a block that leaves fees unswept is refused
System-contract upgrades — bsc overwrites the system contracts' bytecode at every hard fork (before the block's transactions until Feynman, between the ordinary and the system transactions after), and installs the EIP-2935 history contract itself at Pascal The same bytecode, taken from bsc's core/systemcontracts, embedded (zstd-compressed, ~1 MB) and installed at the same points (bnb/upgrades.go)
No block rewards, no beacon root — Parlia pays nothing beyond the fees; its headers carry a zero parent beacon root that bsc skips No ethash rewards on a Parlia chain; the zero beacon root is skipped
PREVRANDAO — bsc never merged, so DIFFICULTY keeps returning the block difficulty (1 or 2) while the post-merge forks are still enabled go-ethereum gates Shanghai+ on a random value, so the block context carries one equal to the difficulty: same opcode result, same rules
Precompiles — BSC adds 0x64–0x69 and prices P256VERIFY (0x100) per RIP-7212 before Osaka BSC's per-fork sets are installed on the EVM and warmed per EIP-2929 (bnb.Config.Precompiles); the three Beacon Chain bridge validators are a pure-Go port, in every historical shape (see The bridge validators)

The chain config itself is go-ethereum's parse of the genesis file (London at Hertz, Shanghai/Cancun/Prague/Osaka by timestamp, bsc's Cancun blob parameters kept through Osaka), so the EVM's own rules — gas tables, opcodes, EIP-7702, EIP-7623 — are upstream's, which bsc tracks.

Verification is the same as on Ethereum — gas used, receipts root and logs bloom against the header — plus two Parlia checks: a block whose bridge-precompile input the port cannot reproduce is refused (see below), and so is one that leaves fees on the system address.

The bridge validators

Three BSC precompiles are the validators of the BNB Beacon Chain bridge: 0x64 (Tendermint header validation), 0x65 (IAVL merkle proof validation) and 0x67 (CometBFT light block validation). The bridge relayed cross-chain packages — validator set updates among them — through them from block 1 until the Beacon Chain was retired (the "Fusion", mid-2024), so state cannot be built from genesis without them. bsc implements them on bnb-chain's forks of Tendermint v0.31 (amino encoding), CometBFT v0.37, IAVL v0.12 and ics23, none of which UMNode can take on as dependencies (CGO-free build, pre-module era code). UMNode carries a pure-Go port of the verification subset instead, package bnb/lightclient: the amino and protobuf wire formats the inputs use, the header and validator-set hashes, commit verification (adjacent and non-adjacent, light and trusting), the IAVL range proofs, the multistore and ics23 commitment proofs, and the consensus states the precompiles read and write.

Old blocks must replay with the behaviour of their time, so every historical shape bsc shipped is reproduced, selected by fork exactly as bsc selects it:

Precompile Shapes
0x64 HEADER_VALIDATE genesis; retired at Nano, back from Moran, retired again at Pasteur
0x65 IAVL_MERKLE_PROOF_VALIDATE genesis (with the verification flaw of the October 2022 bridge exploit, whose blocks must replay), Moran (absence, simple-value and range proofs and duplicated stores forbidden), Planck (ics23 proofs added, the key check), Plato (ics23 only); retired at Pasteur
0x67 COMET_BFT_LIGHT_BLOCK_VALIDATE Luban/Plato (with the inverted validator-set-changed flag), Hertz (the fix), Pasteur (unique validators, per-byte gas)

The port is checked against bsc itself: bsc's own test vectors are replayed (bnb/testdata/lightclient-vectors.json, outputs produced by bsc), and a differential test derives thousands of variants from them — edited trusted states, commit flags, validator sets, proof framing, and byte damage — and compares acceptance and output with bsc's precompiles run through bnb/internal/lcref; bsc's verdicts on a fixed set of 750 variants are pinned in bnb/testdata/lightclient-mutations.json so the comparison also runs without a bsc checkout. At the block level the receipts-root check is the backstop: a divergent verdict changes a receipt's status or logs and the block is refused rather than persisted.

What is not implemented

One input shape the port declines rather than guesses at: a validator in a Tendermint header keyed by a multisig public key, which the Beacon Chain never had. A block whose transaction reaches it is refused with unsupported BSC precompile input, the executor parks, and no diverging state is persisted — the same loud failure the stubs used to give. Genesis-up execution of the live chains has not yet been run end to end against this port; a block it disagrees with bsc on stops the executor at that block.

Also not reproduced, because they only affect block validity (never the state of a valid block): bsc's refusal to run a contract at the validator's own address, the Nano-era address blacklist, and Parlia's header, seal and vote checks — UMNode trusts peer-validated headers on every chain.

Data provenance

  • cfg/bnb/mainnet.json, cfg/bnb/testnet.json: bsc v1.7.8 release genesis.json (allocation, header fields) with the chain config of bsc's params.BSCChainConfig / ChapelChainConfig.
  • bnb/upgrades/: bsc core/systemcontracts/<fork>/<network>/<Contract> at the commit recorded in bnb/upgrades/SOURCE, decoded and zstd-compressed. Regenerate from a bsc checkout with go run bnb/internal/genupgrades/main.go /path/to/bsc.
  • bnb/testdata/bsc-forkid.json: fork ids computed by bsc's own core/forkid at a set of heads, for the fork-schedule tests.
  • bnb/testdata/lightclient-vectors.json: the inputs of bsc's core/vm/contracts_lightclient_test.go and core/vm/lightclient/v2/lightclient_test.go (bsc commit c5533ab5), with the outputs bsc's precompiles give them; lightclient-mutations.json: bsc's verdicts on the variants bnb/lightclient's differential test derives from them. Both come from bnb/internal/lcref, a harness to build inside a bsc checkout; LCREF=/path/to/lcref go test ./bnb/lightclient -run TestDifferential compares a fresh batch, LC_WRITE_GOLDEN=1 re-pins.

When BSC schedules a fork: add its activation to both genesis files (the new …Time key is enough for the fork id), its upgrade table to bnb/upgrades.go if it replaces system contracts (then regenerate the data), and any new precompile or execution rule to bnb/precompiles.go and the executor.