On 2026-09-05 the five properties a follower needs past blocks 17,047,600 and 17,225,968 were added to RUN-A-NODE.md but not to run-node.sh, while the document already claimed the script set them. Our own from-genesis import proof, built from the published revision, synced to block 17,047,400 and then rejected every hybrid anchor 12,352 times: the v2 activation height stayed at Long.MAX_VALUE. Anyone following this package could not pass 17,047,600. Both blocks are now derived from the BESU_OPTS of the network's archive node, whose role is exactly a follower's, with the two public reading nodes as witnesses that must agree on the anchor set; only the signing keys are removed and the registry paths rewritten. Re-measured on the same node and database after the fix: past 17,047,600 with zero rejections.
25 KiB
Run a node on Aere Network mainnet, chain 2800
This file plus genesis.json plus one bootnode enode (see the bootnodes section below) is
everything an outsider needs to bring up a full node that syncs chain 2800 from block 0. Until
2026-08-03 this repository shipped the precompile sources and the NIST vectors but not the chain
configuration, so nobody outside the Foundation could actually start a node, and the "verify it
yourself" claim the repository is built on could not be exercised end to end.
Nothing here is a key. genesis.json is public chain configuration; a node that nobody can dial
is a node nobody can sync from, and the bootnodes section says plainly how dialing works today.
The one command
If you have JDK 21 and git, one command verifies the genesis and the registries by sha256, fetches upstream Besu at the named commit, applies the four mainnet patches in order, builds the node, and starts a full node syncing chain 2800 from block 0:
./run-node.sh --bootnode <enode://...@host:port>
That is the whole setup. The script is deterministic and idempotent: it refuses to run if the genesis or the registries do not match their published sha256, it re-uses a build it already made, and re-running it re-applies nothing and rebuilds nothing. It holds no key and signs nothing. It configures a node that VALIDATES chain 2800, including the post-quantum certificate anchor, but does not PRODUCE seals; producing seals needs a validator Falcon key, which is not in this repository and is not required to follow the chain.
./run-node.sh --help # --data-path, --p2p-port, --rpc-port, --sync-min-peers, --no-start, --rebuild
./run-node.sh --no-start ... # verify, patch and build, but do not start the node
./run-node.sh --sync-min-peers 1 # on a young network with few reachable peers (Besu's default is 5)
You still need a bootnode enode for discovery (see the bootnodes section below for how to get one
today). Everything except peer discovery works without it; if you omit --bootnode the script says
so plainly and stops with instructions rather than starting a node that can never find a peer.
Everything the one command does is also written out by hand below, unchanged, for anyone who wants to audit each step rather than run it.
The four things you need
| What | Where |
|---|---|
genesis.json |
The genesis file, byte for byte as it sits on the Foundation nodes. sha256 361709dccec4e9fc85be5aec33b30b58a55dc53424087d84c1ad11bd1d944c24 (of the published file with LF line endings, exactly as curl fetches it; a Windows checkout that converts line endings will hash differently) |
registries/ |
The public Falcon validator registries. Without them a node imports fine to block 13,014,000 (the first anchor) and then refuses every anchor block, because it cannot map a certificate's seal indices to validator addresses. See registries/README.md; sha256 of each below. |
| a bootnode enode | two published in the bootnodes section below; run-node.sh uses them by default |
patches/, precompiles/ |
The Besu fork, see README.md. Needed to sync past block 9,189,161 |
The registries are public (Falcon public keys and validator addresses, each carrying a proof of
possession and an ECDSA claim; no private key material). Their sha256, also in registries/SHA256SUMS:
006b1b3fb814ed84622539160956263faca7c0b76585c22f2e8acb926c0bcb7f manifest-13014000.json
23736b7d4ba1d3213496f8b5a5f756d5c958b08efebdeb1329b24d7dfa45fb6e manifest-13600000.json
c9a215d294e106f69fb2f16e3af21f9787523969e4b7260a3098f4b2cf458059 registru-2800-v2-13600000.properties
Which Besu
Stock upstream Besu syncs chain 2800 correctly up to block 9,189,161 and then stops being able
to follow it. That block is the first with a timestamp at or after activationTime 1783820272, when
the five Aere post-quantum precompiles come into existence. A node without them computes a different
result for any block containing a call to 0x0AE1 through 0x0AE5 and falls off the chain.
So there are two honest paths:
- Follow the tip. Build the fork: apply
patches/0001-aere-pqc-precompiles-mainnet.patchto upstream Besu commitd2032017bb3b8cb215a97303980a1e4a643f7180and build it. Full instructions, including the pre-image hashes that prove you are on the right base, are inREADME.md. - Verify the early chain only. Stock Besu 26.4 or later will import blocks 0 through 9,189,160 from this genesis. That is enough to check the genesis allocation and the sub-second block period transition against what we publish. It is not enough to reach the head.
There is a second Aere change to the client, and it is not in patch 0001:
patches/0004-aere-basefee-floor.patch, one file, LondonFeeMarket.java. From block 10,141,734
chain 2800 clamps the EIP-1559 base fee to a floor of 1 Gwei. The patch is fork gated by two JVM
system properties and is a byte behaviour no-op when they are unset, so a node that applies it
without setting them is stock Besu. Regenerated 2026-09-05 from the production tree: the previous
revision of this patch hard-coded both values as constants, so on a node built from this recipe the
two properties were dead (the floor was still 1 Gwei from 10,141,734, so the computed base fee was
the same, but the sentence above was not true of the code). Found by comparing the fleet's jars
with a build of this recipe class by class; the three consensus jars were already identical, this
one class was not.
-Daere.basefee.floor.forkBlock=10141734 -Daere.basefee.floor.value=1000000000
Pass those through BESU_OPTS. Apply this patch and set these flags if you want a client that
computes what the network computes. It was measured on 2026-08-02 that QBFT and IBFT2 in Besu do not
validate the base fee at import at all, upstream or in our fork, so a node without this change is
expected to import the chain anyway and only to disagree if it ever proposed a block. That is a
finding about a missing upstream rule and not a licence to skip the patch.
Do not apply patches/0002-...-testnet.patch for a mainnet node. It adds 0x0AE6 and 0x0AE7,
which are not on mainnet, and a node that has them will not disagree with the chain today but is
running a configuration the chain does not.
The post-quantum header anchor: apply patch 0003 to follow the current chain
Since block 13,014,000 every 32nd header of chain 2800 carries a sixth element in its QBFT
extraData: a certificate of validator Falcon-512 seals, universal since block 13,889,296 and
enforced at a minimum of three seals per anchor block (f+1 of nine) since 2026-08-14. A node
rejects an anchor block whose certificate holds fewer than three valid seals. (Correction 2026-08-19:
an earlier version of this paragraph said that from block 14,050,000 the network refuses to finalize
any block without a post-quantum quorum; the per-block rule armed at that height is retired in the
shipped code in favour of the anchor rules and changed no enforcement, so that sentence is withdrawn.)
The anchor ships in this repository both as source, under anchor/, and as
patches/0003-aere-pq-anchor.patch. A node built without it has not been shown to import
headers past block 13,014,000: the second client needed an explicit change to accept the sixth
element, and no floor-only build has been observed following the chain past that height. Apply
0001, 0003, 0004 and 0005 together and treat a build missing any of them as unable to
follow the current chain.
Patch 0005 exists because the import proof was actually attempted
Measured 2026-08-15, on a real from-genesis sync of a build from patches
0001+0003+0004 alone: the node followed chain 2800 for 9.18 million blocks and stopped at
block 9,189,161, the futureEipsTime activation, with failed persisting block due to stateroot mismatch. Not one cause but two, both in MainnetProtocolSpecs.java, both now
carried by patches/0005-aere-eip2935-futureeips.patch: upstream bases the futureEips
milestone on the unreleased amsterdam definition (Block Access Lists, a different state-root
committer), while chain 2800 isolates it on Osaka; and the fleet activates EIP-2935 historical
block hashes in that same fork, one system state write per block from 9,189,161 onward, which
a build without the patch never performs. No amount of green unit tests found this; importing
the real chain found it in one afternoon.
Measured 2026-08-16: the from-genesis sync reached the live tip. A node built only from these
packages imported chain 2800 from block 0 to height 14,088,211 and then followed the head,
crossing the 14,050,000 activation height (which, see above, changed no enforcement) with zero anchor rejections. Two more things
had to be true for that, both now carried by the patches and both described with their measured
boundaries in IMPORT-PROOF-STARE-2026-08-15.md: a historical window (anchor heights
13,267,824 to 13,268,944) in which anchoring was deliberately disarmed fleet-wide, and a recovery
window (13,268,976 to 13,890,544) in which proposers legitimately wrote shorter certificates.
Inside those windows the header is accepted as carried; outside them nothing changes. The
controls run in both directions (641 tests, 0 failures).
Measured 2026-08-15: on a pristine d2032017 checkout, git apply (no flags) accepted
0001, 0003 and 0004 in that order, and ./gradlew --no-daemon installDist -x spotlessJavaCheck -x test exited 0 on JDK 21, producing
besu/v26.8-develop-d203201/linux-x86_64/openjdk-java-21. That proves the recipe builds. Measured again on 2026-09-05 with the regenerated 0003 (169 files: 30 upstream files modified, 139 new, among them the 22 of the SLH-DSA engine): on a pristine d2032017 checkout git apply --check accepted 0001, 0003, 0004 and 0005 in the order run-node.sh uses, and the patched tree equals anchor/ file for file. The installDist build and the from-genesis import were NOT repeated that day; see the revision note below.
The import proof above, measured 2026-08-16, proves the build follows the live chain from genesis
to the tip. The proof is repeated, not assumed: see IMPORT-PROOF-STARE-2026-08-15.md for the
measured heights of each run. Revision note, 2026-09-05: the anchor patch and the configuration
below were updated for the hybrid certificate and the interval schedule; the from-genesis import proof
has NOT yet been repeated against this revision (the last measured run is the one above, on the
previous revision). What is measured for this revision is that the same code follows the live tip on
the network's reading nodes, and validates every hybrid anchor there, since 2026-09-05.
The configuration a follower passes (BESU_OPTS)
The Aere fork reads its per-node configuration from JVM system properties passed through the
BESU_OPTS environment variable, not from the genesis file. run-node.sh sets these for you; this
is what it sets, and what a manual operator must export before the besu command below, or the node
will refuse the first anchor at block 13,014,000. Five of them are the ones without which a node stops
at block 17,047,600 (the first hybrid, v2 certificate) or at the first anchor after 17,225,968 (the
interval change): anchorV2Block, schemeSchedule, hybridRegistry (the SLH-DSA-SHA2-128s public
keys, registries/hibrid-1.properties), anchorIntervalSchedule, and the two dated steps in
anchorMinSeals (see anchor/README.md, scope item 3).
Correction, 2026-09-06. On 2026-09-05 those five properties were added to THIS DOCUMENT and not to
run-node.sh, while the sentence above already claimed the script set them. It did not. A node started with the published script therefore ran the pre-hybrid configuration, and the v2 activation height stayed atLong.MAX_VALUE: our own from-genesis import proof, built from the published revision, synced honestly to block 17,047,400 and then rejected every hybrid anchor 12,352 times (REJECTED - carries a v2 (scheme-tagged) certificate below the v2 activation height 9223372036854775807). Anyone following this package could not pass block 17,047,600. Since 2026-09-06 the block below and the one insiderun-node.share both GENERATED from the liveBESU_OPTSof the network's archive node, which has exactly a follower's role, with the two public reading nodes as witnesses that must agree on the anchor set; a gate refuses any divergence between the chain, the script and this page. Re-measured after the fix on the same node and database: past 17,047,600 with zero rejections.
The set below is the one the network's own archive node runs with, minus the signing keys.
REG=/path/to/aere-node/registries
export BESU_OPTS="\
-Daere.basefee.floor.forkBlock=10141734 \
-Daere.basefee.floor.value=1000000000 \
-Daere.falcon.anchor.address=0x5214F0e53899Dad805ef3AdF0624F5e49d2a5EB5 \
-Daere.falcon.anchor.block=13889290 \
-Daere.falcon.attachBlock=13889296 \
-Daere.falcon.attachInterval=32 \
-Daere.falcon.forkBlock=14050000 \
-Daere.falcon.manifest=$REG/manifest-13600000.json \
-Daere.falcon.minAttachFutureMargin=5 \
-Daere.falcon.registry.history=$REG/manifest-13014000.json,$REG/registru-2800-v2-13600000.properties \
-Daere.falcon.validatorCount=9 \
-Daere.pq.anchorBlock=13014000 \
-Daere.pq.anchorInterval=32 \
-Daere.pq.anchorIntervalSchedule=17225968:128 \
-Daere.pq.anchor.maxSeals=9 \
-Daere.pq.anchorMinSeals=13014000:0,13034000:3,14961456:6,17102384:0,17102416:6 \
-Daere.pq.anchorV2Block=17047600 \
-Daere.pq.chainId=2800 \
-Daere.pq.hybridRegistry=$REG/hibrid-1.properties \
-Daere.pq.schemeSchedule=17047568:falcon-512+slh-dsa-sha2-128s \
-Daere.pq.anchor.minSealsCeiling=1"
Why the seal-threshold ceiling, for a from-genesis node
A node syncing from block 0 starts into the GENESIS validator set, which is small (three
validators). The startup threshold guard (PqAnchorThresholdGuard) refuses to arm a seal threshold
at or above that set's QBFT quorum, because a proposer at that set size could never gather it. The
published schedule reaches K=3 (block 13,034,000) and K=6 (block 14,961,456), both unreachable at
N=3, so without aere.pq.anchor.minSealsCeiling a from-genesis node refuses to start with
AERE-PQC-THRESHOLD-01. The ceiling lowers the EFFECTIVE threshold to what the genesis set supports
(quorum(3) - 1 = 1) WITHOUT editing the published schedule, exactly as that guard's own message
instructs (--Xaere-pq-anchor-min-seals-max). It does not weaken the post-quantum binding a
follower checks: every anchor must still carry a valid validator Falcon seal that verifies against
the registry, and its digest must still bind the certificate under the block hash; only the
seal-COUNT floor, which is a proposer-liveness property relative to the current set and not a
history-verification property, is relaxed to one. Real anchor blocks carry three to nine seals, so
all import cleanly. A validator, which starts into the full set and proposes, does not set this.
This is the FOLLOWER subset, and the distinction is in the code under anchor/, not guessed. A
validator additionally passes aere.falcon.key (the signing key), the aere.falcon.attach* and
aere.falcon.validatorCount properties (these drive seal PRODUCTION only), and
aere.falcon.forkBlock (a per-block Falcon rule the shipped code retires at the anchor block, so it
is inert; leaving it unset keeps the non-blocking baseline). What is above is exactly what the
IMPORT path reads: the registries (aere.falcon.manifest loads only together with
aere.falcon.anchor.address, so those two are registry wiring here, not attach knobs;
aere.falcon.registry.history carries every earlier registry the chain was signed against), the
anchor-validation schedule (aere.pq.*), and the 1 Gwei base-fee floor live from block 10,141,734.
Start it
Export BESU_OPTS as above first, then:
besu \
--genesis-file=/path/to/aere-node/genesis.json \
--data-path=/path/to/data \
--network-id=2800 \
--sync-mode=FULL \
--data-storage-format=BONSAI \
--bootnodes=<the enode you were given> \
--p2p-port=30303 \
--rpc-http-enabled --rpc-http-port=8545 \
--rpc-http-api=ETH,NET,WEB3,QBFT \
--min-gas-price=0
Two bootnodes are published here since 2026-08-25. Both are public read hosts that are NOT in the validator set, so taking them down cannot cost the network consensus; their addresses were already public through the services they serve. The first runs Besu, the second runs Nethermind, so your first two peers are also two independent implementations:
enode://7e8ff740b79bf28a6d7e46ea8d2317cea4a223e7af60be36e91e13dc7051f4cb29077631452e699a5e18e640a94fd6e370e9e51dd91ecba1f2ba9666e07ff01b@37.27.216.110:30303
enode://a4b90d4f5fc7758814d41acfe00f6a2516683f3d3052410091b894d014dbb2184d6158da76f72e84ca268c2ccde3590d5686f978868f3b6ad6af87e398179fb8@157.180.67.185:30303
run-node.sh uses both by default when --bootnode is not given. A written address is not a
live address: only your own net_peerCount proves they are up. If both are unreachable, write
to office@aere.network with the subject "bootnode" for a current enode by return.
--sync-mode=FULL is deliberate. Chain 2800 runs QBFT with a half second block period, so the head
was about 12.2 million blocks when this was written (2026-08-03), passed 14.6 million on 2026-08-19,
and climbs by roughly two per second; read the live height with eth_blockNumber. Full sync from block 0 is the mode
that actually verifies the history we publish, which is the point of this repository. Snap sync
against a QBFT chain trusts a peer for the state and verifies far less.
--min-gas-price=0 matches the network. Chain 2800 has a real 1 Gwei base fee from block
10,141,734, but the minimum gas price a node will accept into its own pool is a local policy and the
Foundation nodes run it at zero.
Outbound and inbound TCP and UDP on your p2p port must be open. Discovery is UDP; a TCP only firewall gives you a node that dials out and is never dialled back.
Check that you are on the right chain, before you trust anything else
Two facts settle it. Run these against your own node once it is up.
1. Your block 0 must be the network's block 0.
curl -s -X POST http://127.0.0.1:8545 -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["0x0",false]}' \
| grep -o '"hash":"0x[0-9a-f]*"'
Expected, and this is the whole test:
0xd86d57a899cbfa580669f0e1c7cd4ae5f525247c3c823f8f75df176a0c5d7f1a
That hash covers every consensus relevant field in genesis.json, including the state root of the
six genesis accounts. If it matches, your genesis file is the network's genesis file and no further
comparison of individual fields is needed. If it does not match, your node will never peer, and no
amount of correct bootnodes will help.
Compare against the public endpoints, which are separate machines with separate operators of the same file:
curl -s -X POST https://rpc.aere.network -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["0x0",false]}'
curl -s -X POST https://rpc2.aere.network -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["0x0",false]}'
2. You must have peers.
curl -s -X POST http://127.0.0.1:8545 -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"net_peerCount","params":[]}'
A nonzero answer means discovery worked. Zero after a few minutes means either your UDP port is closed or the bootnode you were given is down; a plain TCP connect to its address distinguishes the two.
What the two bootnodes are, and what they are not
They are two full nodes that are not in the validator set. Chain 2800 seals blocks with nine QBFT validators (seven until 2026-08-09), and none of the nine is listed here, on purpose. Publishing a validator's p2p address hands an attacker a target whose downtime costs the network consensus headroom, which is why sentry style architectures exist. The two nodes above carry no consensus responsibility: if both are taken down, new nodes cannot bootstrap until they are replaced, and block production is unaffected.
Update, 2026-08-28. The first bootnode (37.27.216.110) now also serves the devp2p
snap/1 protocol (SnapServer ... Bonsai full flat db in its startup log, capability list
[eth/68..71], [istanbul/100], [snap/1]). A client that chooses --sync-mode=SNAP against it
downloads headers plus the state at the pivot instead of replaying every historic block body,
which on this chain of empty blocks removes most of the multi-day full-sync cost. Honest limits,
measured versus not: the server capability and the public endpoint answering are measured today;
a complete stranger's snap sync to head against it has not yet been run, and the one-command
run-node.sh path deliberately stays --sync-mode=FULL, because a validating node that replayed
every block is a stronger statement than one that trusted a pivot.
A written address is not a live address. This file cannot prove the bootnodes are up at the moment
you read it; only your own net_peerCount can.
Both were reachable and serving blocks when this file was written. Both are Foundation operated, so this is one operator's word for two addresses. That is a real centralisation fact about chain 2800 today and not a documentation gap.
Measured, 2026-08-03
The recipe on this page was executed rather than written. A Besu node with an empty data
directory was started from exactly this genesis.json and exactly these two bootnode URLs, on a
machine that is not a validator, with --sync-mode=FULL and no static nodes, no other peer hints
and no copied database.
- Its own block 0 came out as
0xd86d57a899cbfa580669f0e1c7cd4ae5f525247c3c823f8f75df176a0c5d7f1a, which is the live chain's block 0. That hash covers the state root, so the six accounts inallocare the network's down to the last wei, and so is every header field in the file. - It reached 11 peers knowing nothing but the two enode URLs above.
- It imported from block 0 to block 2,221,580 in about thirty minutes, with zero rejected blocks, which carries it through the sub-second block period change at roughly 2,137,652 and out the other side.
The honest limits on that run, stated because they are the difference between this section and an advertisement:
- It was stopped at ~2.2 million rather than carried to the head, so it never reached the precompile fork at 9,189,161 or the base fee floor at 10,141,734. "Reaches the current head" is not measured.
- It ran the Foundation's build of the fork, not a build a third party made from
patches/. That those patches build, and that the built precompiles answer the NIST vectors, is measured separately inREADME.md; that the two together sync to the head is not. - Nothing here proves the bootnodes will be up when you read this.
A number in this file that the chain does not agree with
config.transitions.qbft[0].block reads 2138451. That is the height at which the file says the
block period drops from one second to half a second. The chain says otherwise. Sampling header
timestamps on the public endpoints puts the change just after 2,137,652. Measured spans of
twenty blocks: from 2,137,600 the twenty blocks take 20 s, from 2,137,656 they take 15 s, from
2,137,657 they take 12 s, and from 2,137,700 they take a flat 10 s, which is 500 ms a block and
stays there through 2,138,400. The published 2138451 is roughly eight hundred blocks late and
describes a boundary that is not in the data.
It is published anyway, unaltered, because this file is the file the network runs and its value as evidence comes from being exactly that. Two things make the discrepancy harmless to you rather than merely tolerated: the field only governs how fast a proposer may seal, so it has no effect on a node that follows; and it was measured, on the sync run described above, that a node carrying this exact value imports the blocks either side of both numbers without rejecting one.
Correcting it means editing a file on live validators, which is a change to running consensus infrastructure and is not made as a side effect of publishing a document.
A note on the comment fields in alloc
The six genesis accounts carry comment strings. No client reads them; they are not consensus data
and they are inside the file only because they have always been. Two of them are wrong and are left
uncorrected here rather than quietly edited, because this file is published byte for byte as the
running nodes hold it: the dash characters are mangled, and the 1,400,000,000 AERE account is
labelled "Mining Reserve" although chain 2800 has no mining and never has. It is the staking and
emissions reserve. Correcting the text means editing a file on live nodes, which is a change to
running infrastructure and is not made as a side effect of a documentation fix.