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 upstreameth/70dropped:[version, networkid, td, genesis, forkid, earliest, latest, latestHash]. Block ranges, headers, bodies and theeth/70receipts messages are otherwise upstream's.eth/68— after the standard status exchange, bsc sends anUpgradeStatusmessage (code0x0b) 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 releasegenesis.json(allocation, header fields) with the chain config of bsc'sparams.BSCChainConfig/ChapelChainConfig.bnb/upgrades/: bsccore/systemcontracts/<fork>/<network>/<Contract>at the commit recorded inbnb/upgrades/SOURCE, decoded and zstd-compressed. Regenerate from a bsc checkout withgo run bnb/internal/genupgrades/main.go /path/to/bsc.bnb/testdata/bsc-forkid.json: fork ids computed by bsc's owncore/forkidat a set of heads, for the fork-schedule tests.bnb/testdata/lightclient-vectors.json: the inputs of bsc'score/vm/contracts_lightclient_test.goandcore/vm/lightclient/v2/lightclient_test.go(bsc commitc5533ab5), with the outputs bsc's precompiles give them;lightclient-mutations.json: bsc's verdicts on the variantsbnb/lightclient's differential test derives from them. Both come frombnb/internal/lcref, a harness to build inside a bsc checkout;LCREF=/path/to/lcref go test ./bnb/lightclient -run TestDifferentialcompares a fresh batch,LC_WRITE_GOLDEN=1re-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.