524 lines
33 KiB
Markdown
524 lines
33 KiB
Markdown
# 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 five things you need
|
|
|
|
| What | Where |
|
|
|---|---|
|
|
| `genesis.json` | The genesis file, byte for byte as it sits on the Foundation nodes. sha256 `e5038e6a1437bd85fa51294609edf2126a997f14f4fa7a65a0d39bd0aa24d2e5` (of the published file with LF line endings, exactly as `curl` fetches it; a Windows checkout that converts line endings will hash differently) Updated 2026-09-10: the fleet genesis gained a third `config.pqRegistryHash` entry at 18,082,816 (block-0 hash unchanged; only `config` changed), so the file and its digest changed on that date. |
|
|
| `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 | one published in the bootnodes section below; run-node.sh uses it by default |
|
|
| `patches/`, `precompiles/` | The Besu fork, see `README.md`. Needed to sync past block 9,189,161 |
|
|
| **a clock within one second of real time** | not a file, but as hard a requirement as the four above. See below |
|
|
|
|
**Your clock, and why it is on that list.** QBFT header validation on this chain is
|
|
`TimestampBoundedByFutureParameter(1)`: a header whose timestamp is more than **one second** ahead of
|
|
your node's clock is invalid. Blocks here are ~0.6 s apart, so a machine running one second slow sees
|
|
every fresh head as "from the future" - and Besu does not treat that as a clock disagreement. It
|
|
treats it as `BREACH_OF_PROTOCOL` and disconnects the peer that served it:
|
|
|
|
```
|
|
INFO | TimestampBoundedByFutureParameter | Invalid block header:
|
|
timestamp 1789189254 is greater than the timestamp margin 1789189253
|
|
WARN | PipelineChainDownloader | Invalid block detected (BREACH_OF_PROTOCOL).
|
|
Disconnecting from sync target.
|
|
```
|
|
|
|
If you see that, nothing is wrong with the chain and nothing is wrong with this package. Your clock is
|
|
behind. The node recovers on its own ("Chain download failed. Restarting after short delay.") and will
|
|
still reach the head, but it drops good peers while it does. Run NTP - and check it actually took:
|
|
measured 2026-09-12 on our own import run, a machine where `timedatectl` reported
|
|
`NTPSynchronized=yes` was still a full second behind, because it was a VM whose host clock was right
|
|
and whose own was not. Compare against the chain itself rather than against a status flag:
|
|
|
|
```
|
|
curl -s -X POST -H 'content-type: application/json' \
|
|
-d '{"jsonrpc":"2.0","id":1,"method":"eth_getBlockByNumber","params":["latest",false]}' \
|
|
https://rpc.aere.network | grep -o '"timestamp":"0x[0-9a-f]*"'
|
|
```
|
|
|
|
Convert that hex to decimal and compare it with `date +%s` on your machine. A difference of 0 or 1 is
|
|
fine. More than that, in either direction, and you will see the behaviour above.
|
|
|
|
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
|
|
c6601ae5159875a090066946991517c9a38e5d6206da76b018c0917e1ca6f12f manifest-18082816.json
|
|
35d69f60e357d405aff39fd33858de901dfb21327b0c4aeb18be17ffdaa115d6 hibrid-10.properties
|
|
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.patch` to
|
|
upstream Besu commit `d2032017bb3b8cb215a97303980a1e4a643f7180` and build it. Full instructions,
|
|
including the pre-image hashes that prove you are on the right base, are in `README.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. **Revision note, 2026-09-10:** patches `0001`, `0003` and `0004` were regenerated from the production tree over the same file sets (the source files now carry the Apache-2.0 section 4(b) modification notices that production carries; `0003` keeps its 169 files). Measured the same day: a build from this recipe reproduces the fleet's three consensus jars class by class, and `LondonFeeMarket` and the precompile classes byte for byte. What the recipe still does not ship is the fleet's parallel block processor (a separate package and one hunk in `MainnetProtocolSpecs`), which does not change consensus; it is listed by the comparison, not hidden.
|
|
|
|
---
|
|
|
|
## Sizing the JVM heap: bigger is not faster
|
|
|
|
Measured 2026-09-06 on one machine, one database and one chain, syncing chain 2800 from genesis in a
|
|
Linux VM with 8 GB of RAM. The same flag was measured at four values and at two different heights,
|
|
because the answer changes with the height:
|
|
|
|
| `-Xmx` | height reached | what the JVM did | blocks per minute |
|
|
| --- | --- | --- | --- |
|
|
| 4500m | ~3.5 M | 89 `OutOfMemoryError`, 7.1 GB RSS, import stopped dead | 0 |
|
|
| 3200m | ~6 M | 24 OOM per 500 log lines, repeated `Chain download failed` | ~1,500 |
|
|
| 2400m | 6-8 M | no OOM at all | ~82,000 |
|
|
| 2400m | past 9.2 M | old generation 99.88% full, 1,409 full GCs, 19 minutes in GC | ~640 |
|
|
| 3400m | past 9.7 M | **zero** full GCs, 6 seconds of GC in total | ~28,800 |
|
|
| 2400m | ~8.4 M, measured 2026-09-11 | old generation 99.97% full, 1,073 full GCs, 26 minutes in GC, 49 `OutOfMemoryError` | ~120 |
|
|
| 3400m | ~8.5 M, measured 2026-09-11 | **zero** full GCs | ~9,000 |
|
|
|
|
**The band moves down as the chain grows, so read the dates in that table.** The 2400m row measured on 2026-09-06 held from
|
|
six to eight million blocks; the same flag on 2026-09-11, at 8.4 million and on a 7.9 GB machine, was already in full-GC collapse,
|
|
because the storage engine's working set had grown with the chain in between. Do not copy a number out of this table and hope. Use
|
|
the symptom instead, it is the same on any day: full GCs climbing while the height barely moves means the heap is too SMALL, and
|
|
`OutOfMemoryError` in the log means it is too LARGE for what the database also needs beside it.
|
|
|
|
Two things are going on, and they pull in opposite directions. **Heap is not memory**: the storage
|
|
engine keeps its block cache and write buffers OUTSIDE the Java heap, and past six million blocks that
|
|
off-heap part is larger than the heap itself, so a heap sized to fill the machine leaves nothing for it
|
|
and the node dies of `OutOfMemoryError`. But a heap that is too small is just as bad in the other
|
|
direction: past block 9,189,161, where the post-quantum precompiles go live, blocks get heavier, the old
|
|
generation fills, and the node spends its time in full GC instead of importing.
|
|
|
|
**How much RAM a from-genesis import needs, measured.** The requirement is not flat: it grows with the
|
|
height, because the storage engine's working set grows with the chain. On the machine above (8 GB for
|
|
the node), the same import that ran comfortably at three million blocks was, at 12.2 million, using
|
|
5.96 GB of resident memory with a 3.4 GB heap, with the old generation 99.8% full and four full GCs
|
|
every five seconds - and there was no room left to raise the heap without hitting `OutOfMemoryError`
|
|
instead. Treat 8 GB as the floor at which a full from-genesis import stops being practical, not as a
|
|
comfortable size; 16 GB gives the database the room this measurement shows it wants. Following the
|
|
live tip, once synced, needs far less: the network's own reading nodes do it inside their normal
|
|
footprint.
|
|
So do not pick a fraction of RAM and hope. Watch it: `jstat -gcutil <pid> 5000 3` prints one line every
|
|
five seconds. If `FGC` climbs while the height barely moves, the heap is too SMALL - raise it. If you see
|
|
`OutOfMemoryError` in the log, it is too LARGE for what the database also needs - lower it. On 8 GB, the
|
|
band that worked here was roughly 2.4 GB early and 3.4 GB past nine million blocks.
|
|
## 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-10.properties` since 2026-09-10; `hibrid-1.properties` before), `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 at `Long.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 inside `run-node.sh` are both GENERATED from the live
|
|
> `BESU_OPTS` of 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.
|
|
|
|
```bash
|
|
REG=/path/to/aere-node/registries
|
|
export BESU_OPTS="\
|
|
-Daere.basefee.floor.forkBlock=10141734 \
|
|
-Daere.basefee.floor.value=1000000000 \
|
|
-Daere.falcon.anchor.address=0xC01Bb2843EFAF92D8389F14aC1dD26dbeAf9144C \
|
|
-Daere.falcon.anchor.block=13889290 \
|
|
-Daere.falcon.attachBlock=13889296 \
|
|
-Daere.falcon.attachInterval=32 \
|
|
-Daere.falcon.forkBlock=14050000 \
|
|
-Daere.falcon.manifest=$REG/manifest-18082816.json \
|
|
-Daere.falcon.minAttachFutureMargin=5 \
|
|
-Daere.falcon.registry.history=$REG/manifest-13014000.json,$REG/registru-2800-v2-13600000.properties \
|
|
-Daere.falcon.validatorCount=10 \
|
|
-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-10.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 far more than one seal, so
|
|
all import cleanly: measured 2026-09-12 by our own second implementation walking the live chain, the
|
|
certificates carried 14, 16, 18 or 20 seals in total, which is seven to ten per half since the
|
|
certificate became hybrid on 2026-09-03. A validator, which starts into the full set and proposes,
|
|
does not set this.
|
|
|
|
**What you will see in the log, and why it is not an incident.** With this ceiling set, the node
|
|
prints a red startup banner (`AERE-PQC-EMG-BANNER-01`, "starting with 1 POST-QUANTUM SAFETY
|
|
CONTROL lowered") and then one ERROR line **per anchor block**:
|
|
|
|
```
|
|
ERROR | PqEmergencyShoutRule | AERE PQC EMERGENCY [AERE-PQC-EMG-CEILING-01]: height N validated
|
|
WITH A LOWERED SEAL THRESHOLD. The schedule in force asks for K=...
|
|
```
|
|
|
|
That is the binary doing what we asked it to do: a safety control that has been lowered must be
|
|
impossible to forget, so it shouts on every use rather than once at startup. On a from-genesis sync
|
|
it is loud - measured on our own import run, 2,786 such lines in three megabytes of log. It is not a
|
|
chain fault and not a failed verification.
|
|
|
|
What WOULD be a fault, and what you should watch for instead: an anchor **refused**. Those read
|
|
`IneligibleSeal`, `SealCountBelowThreshold`, or `AnchorDigestMismatch` and stop the import. If you
|
|
prefer a quiet log once you are past the early heights, restart the node without
|
|
`aere.pq.anchor.minSealsCeiling` after it is synced beyond block 13,034,000: by then the live
|
|
validator set is large enough for the scheduled threshold to arm, and the shouting stops because the
|
|
control is no longer lowered.
|
|
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
```
|
|
|
|
One bootnode is published here (two from 2026-08-25 to 2026-09-11; the second address was withdrawn on 2026-09-11
|
|
because its host no longer fits the rule in the section below, and a replacement public read host will be
|
|
published when it exists). It is a public read host that is NOT in the validator set, so taking it down
|
|
cannot cost the network consensus; its address was already public through the service it serves. It runs Besu:
|
|
|
|
```
|
|
enode://7e8ff740b79bf28a6d7e46ea8d2317cea4a223e7af60be36e91e13dc7051f4cb29077631452e699a5e18e640a94fd6e370e9e51dd91ecba1f2ba9666e07ff01b@37.27.216.110:30303
|
|
```
|
|
|
|
`run-node.sh` uses it by default when `--bootnode` is not given. A written address is not a
|
|
live address: only your own `net_peerCount` proves it is up. If it is unreachable, write
|
|
to office@aere.network with the subject "bootnode" for a current enode by return.
|
|
|
|
Measured 2026-09-11: a node that cannot receive inbound
|
|
connections (behind NAT without a forwarded port) keeps that single peer, and Besu does not fetch the
|
|
bodies of blocks that carry transactions from a single busy peer: the log repeats
|
|
`Unable to retrieve blocks for block numbers ... Restarting after short delay` and the import crawls.
|
|
Forward your P2P port (30303 by default) and, when you run the manual command below, add
|
|
`--p2p-host=<your public IP>` so that peers can bond with you; once discovery works the node reaches the
|
|
rest of the network on its own (measured the same day with ten peers: about 1,300 blocks per second
|
|
from genesis, no body failures). With a single published bootnode this matters more, not less.
|
|
|
|
`--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.**
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.**
|
|
|
|
```bash
|
|
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 bootnode is, and what it is not
|
|
|
|
It is a full node that is **not** in the validator set. Chain 2800 seals blocks with ten QBFT
|
|
validators (nine from 2026-08-09, seven before; ten since 2026-09-11), and none of them 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 node above carries no consensus responsibility: if it is
|
|
taken down, new nodes cannot bootstrap until it is replaced, and block production is unaffected.
|
|
|
|
**Update, 2026-08-28.** The 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 bootnode is 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 in `alloc` are 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 in `README.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.
|