Reunite the two lines of this repository: the runnable package joins the anchor

The published line delivered the anchor (source and patch 0003) but had lost
the chain configuration: no genesis, no run recipe. The unpublished local line
had genesis.json, RUN-A-NODE.md and the measured claims, but predated the
anchor. This commit carries the package files onto the anchor line:

- genesis.json restored, byte for byte as the running nodes hold it
- RUN-A-NODE.md restored; the basefee-floor patch returns as 0004; the anchor
  section states plainly that a build without patch 0003 has not been shown to
  import headers past block 13,014,000, and that an import proof for a
  patches-only build is the next thing this repository owes its reader
- bootnodes are deliberately NOT published: today's entry points are consensus
  or origin hosts, and this project does not print its machines' addresses; a
  dedicated bootnode host is the planned replacement, an enode is available by
  mail until then
This commit is contained in:
Aere Network 2026-08-15 13:18:55 +03:00
parent 3f62a21a70
commit a70d06fef6
5 changed files with 640 additions and 0 deletions

21
CITATIONS-UNRESOLVED.md Normal file
View File

@ -0,0 +1,21 @@
# Citations in this repository that you cannot open
This file is generated by the legaturi-repara.cjs script in the Aere working tree and
enforced by legaturi.cjs. It is the complete list, measured on 2026-08-11, of every
path cited in this repository that does not resolve to a published file.
A citation is a promise that a claim is checkable. Where the target is not published,
the claim is traceable but **not independently verifiable by you**, and it should be
read as our assertion. Nothing was deleted to produce this list: every path below is
still cited in the text exactly as our authors wrote it.
Paths that DO resolve are written relative to a side-by-side checkout of the Aere
repositories, so they begin with a repository name, for example
`aere-contracts/contracts/pqc/AereCryptoRegistry.sol`.
Unresolvable distinct paths in this repository: **4**.
- `aerenew/docs/DOVADA-PQC-VERIFICABILA-2026-07-20.md` cited in: aere-node/README.md
- `CHANGELOG.md` cited in: aere-node/README.md
- `LondonFeeMarket.java` cited in: aere-node/RUN-A-NODE.md
- `static-nodes.json` cited in: aere-node/README.md

84
LICENSING.md Normal file
View File

@ -0,0 +1,84 @@
# Licensing
Default for this repository: **Apache License 2.0**, `Copyright 2026 AERE Network`. See `LICENSE`
and `NOTICE`.
This repository is the one Aere Network repository that is not MIT, and the reason is not style.
It contains code derived from Hyperledger Besu, and Besu is Apache-2.0. A permissive licence on
top of Apache-2.0 material is not a free choice: Apache-2.0 section 4 sets conditions on anyone who
redistributes, and those conditions are what this page records.
Where a file has its own `SPDX-License-Identifier`, **that header governs.** Every source file in
this repository carries one, and every one of them says `Apache-2.0`.
## Who wrote what, as measured
Read from the copyright line of every file under `precompiles/`.
| File | Copyright header | Origin |
| --- | --- | --- |
| `precompiles/Address.java` | `Copyright contributors to Hyperledger Besu.` | upstream, **modified by Aere Network** |
| `precompiles/MainnetPrecompiledContracts.java` | `Copyright contributors to Hyperledger Besu.` | upstream, **modified by Aere Network** |
| `precompiles/AereFalconSupport.java` | `Copyright contributors to the AERE Network.` | new, Aere Network |
| `precompiles/Falcon512PrecompiledContract.java` | `Copyright contributors to the AERE Network.` | new, Aere Network |
| `precompiles/Falcon1024PrecompiledContract.java` | `Copyright contributors to the AERE Network.` | new, Aere Network |
| `precompiles/MLDSA44PrecompiledContract.java` | `Copyright contributors to the AERE Network.` | new, Aere Network |
| `precompiles/SLHDSA128sPrecompiledContract.java` | `Copyright contributors to the AERE Network.` | new, Aere Network |
| `precompiles/SHAKE256PrecompiledContract.java` | `Copyright contributors to the AERE Network.` | new, Aere Network |
| `precompiles/MLKEM768PrecompiledContract.java` | `Copyright contributors to the AERE Network.` | new, Aere Network |
| `precompiles/HashToPointPrecompiledContract.java` | `Copyright contributors to the AERE Network.` | new, Aere Network |
Two upstream files, eight new ones. The diffs under `patches/` apply against upstream Besu commit
`d2032017bb3b8cb215a97303980a1e4a643f7180` and touch exactly those same two upstream files.
The eight new files are Aere Network's copyright and are still Apache-2.0 rather than MIT. They are
written to compile inside a Besu tree and they live in Besu's own package namespace,
`org.hyperledger.besu.evm.precompile` and `org.hyperledger.besu.datatypes`. Licensing them to match
the tree they are built for is what makes them usable where they are meant to be used.
## How this repository meets Apache-2.0 section 4
| Condition | Where it is met |
| --- | --- |
| **4(a)** give recipients a copy of the License | `LICENSE`, the full Apache-2.0 text |
| **4(b)** modified files carry prominent notices stating you changed them | a `Modifications Copyright 2026 Aere Network` block at the top of `precompiles/Address.java` and `precompiles/MainnetPrecompiledContracts.java`, naming what was changed and against which upstream commit. The same block is inside the diff bodies under `patches/`, so a tree with the patches applied carries it too. The two files are also named in `NOTICE` and in the patch commit messages |
| **4(c)** retain all copyright, patent, trademark and attribution notices from the source | `Address.java` and `MainnetPrecompiledContracts.java` keep their upstream `Copyright contributors to Hyperledger Besu.` headers unchanged |
| **4(d)** carry a readable copy of the upstream `NOTICE` attributions | `NOTICE`, which reproduces Besu's `NOTICE` in full, including the Tuweni and Apache Software Foundation attributions it in turn carries |
### Where the 4(b) notice lives, and a wrong turn we took first
The notice is a `Modifications Copyright` comment block at the top of each of the two modified files,
placed below the upstream copyright header and above the `package` declaration. The upstream header
itself is untouched, because 4(c) requires that; 4(b) is satisfied by the separate block underneath.
This is the shape OpenSearch uses on the Elasticsearch files it inherited, and it is the shape a
reader looking at a single copied file will actually see.
An earlier version of this page argued that the notice had to live in the patch commit message
instead, because putting it in the file would break the claim that `precompiles/` is byte-identical
to what the patches install. That argument was wrong, and it was wrong in an avoidable way: it
treated the patch as fixed. Put the notice **inside the diff body** and both properties hold at once.
The patch installs a file that already carries the notice, and `precompiles/` still matches it byte
for byte. There was never a trade to make.
The patch commit messages still describe the change, which is useful under `git am`, but nothing now
depends on the reader using `git am` rather than `git apply`.
## What is measured here
The byte-identity claim used to be recorded on this page as NOT MEASURED. It has now been measured,
on 2026-08-02, and the result is in `README.md` under "The patch": the base was confirmed against the
`index` pre-image hashes in patch `0001`, both patches applied at exit 0, all ten files that the
patches create or modify were compared against `precompiles/`, and the comparison found one real
mismatch. `Address.java` and `MainnetPrecompiledContracts.java` after **both** patches are not what
this repository ships; this repository ships them at the `0001` state, because `0x0ae6` and `0x0ae7`
are testnet-only. That is deliberate, and `README.md` now says so instead of claiming otherwise.
Still **NOT MEASURED**: whether the sources in `precompiles/` are the sources that built the binary
currently running on the validators. Nothing on this page or in `README.md` asserts a hash for that.
## Aere Network's other repositories
The rest of Aere Network's public code is MIT. This repository is Apache-2.0 because of what is in
it, not because of a different policy. If you are copying an Aere Network component and want to know
which licence applies, read the `LICENSE` file of the repository you are copying from, and the
`SPDX-License-Identifier` header of the file you are copying, which wins over both.

235
RUN-A-NODE.md Normal file
View File

@ -0,0 +1,235 @@
# 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 three things you need
| What | Where |
|---|---|
| `genesis.json` | The genesis file, byte for byte as it sits on the Foundation nodes. sha256 `6f56eacada94457c1aa325c878399989e344dcf1d763e2d3b479f9b068784ed1` |
| a bootnode enode | requested by mail for now; see the bootnodes section below |
| `patches/`, `precompiles/` | The Besu fork, see `README.md`. Needed to sync past block 9,189,161 |
---
## 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:
```
-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 since 2026-08-14. From block 14,050,000 the
network refuses to finalize blocks without that post-quantum quorum.
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. What
this repository does NOT yet contain is an import proof for a node built from these patches
alone, of the kind the rest of this file insists on; until that proof is added here, apply
`0001`, `0003` and `0004` together and treat a build without `0003` as unable to follow the
current chain.
---
## Start it
```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
```
Bootnodes are deliberately NOT published in this repository yet. The network entry points
available today are consensus or origin hosts, and this project does not print the addresses
of its own machines in public documents; a dedicated public bootnode, on a host that serves
nothing else, is the planned replacement, and this paragraph will then carry its enode. Until
then, write to office@aere.network with the subject "bootnode" and you will receive a current
enode by return. Everything else on this page works without one; discovery is the only step
that needs it.
`--sync-mode=FULL` is deliberate. Chain 2800 runs QBFT with a half second block period, so the head
is around twelve million blocks and climbing by two per second. 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 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 seven
QBFT validators, and none of the seven 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.
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 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.

77
genesis.json Normal file
View File

@ -0,0 +1,77 @@
{
"config": {
"chainId": 2800,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"muirGlacierBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"shanghaiTime": 0,
"qbft": {
"blockperiodseconds": 1,
"epochlength": 30000,
"requesttimeoutseconds": 4
},
"cancunTime": 1780189051,
"pragueTime": 1780189051,
"osakaTime": 1780220351,
"futureEipsTime": 1783820272,
"transitions": {
"qbft": [
{
"block": 2138451,
"xblockperiodmilliseconds": 500
}
]
},
"pqRegistryHash": [
{
"block": 13014000,
"hash": "0xa96ac96dcd4f1671aec8b6063cee17a3341314c92e1dd2623bc6500b7e28b0cb"
},
{
"block": 13600000,
"hash": "0x5e6ac058b22beaa895469671308548dfd44fbaa9ca7ea30b3dafa950c359b9b3"
}
]
},
"nonce": "0x0",
"timestamp": "0x69fbd960",
"gasLimit": "0x1fffffffffffff",
"difficulty": "0x1",
"mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
"coinbase": "0x0000000000000000000000000000000000000000",
"extraData": "0xf865a00000000000000000000000000000000000000000000000000000000000000000f83f94b5e768cb3117ea25175f51aa956b48691025c2a894f73bbbd26a82dbc769066550d8d4585eab2fc00d944bf6f91d84b37454544279cba152581ed6820044c080c0",
"alloc": {
"0xaee2f3989f0AB23296Fa3b92247fe67587141311": {
"balance": "0x52b7d2dcc80cd2e4000000",
"comment": "Strategic Investor \u00e2\u20ac\u201d 100,000,000 AERE (3.57% of supply)"
},
"0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3": {
"balance": "0x94e47b8d68171534000000",
"comment": "Foundation \u00e2\u20ac\u201d 180,000,000 AERE (operations + signing wallet)"
},
"0x038f59A40ceeCd599A4588E4B0ff4642a0fbfFB8": {
"balance": "0x4860d8812f0b38878000000",
"comment": "Mining Reserve \u00e2\u20ac\u201d 1,400,000,000 AERE (50% of supply, mining emissions)"
},
"0xB6a364F47d21DC2CbEB803565c111c1026e11C75": {
"balance": "0x1cf389cd46047d030000000",
"comment": "Ecosystem Reserve \u00e2\u20ac\u201d 560,000,000 AERE (20% of supply, grants + partnerships)"
},
"0x7968C438204a78B4e032fcFFd9A56Edb15fdCCdf": {
"balance": "0x15b6a759f4835dc24000000",
"comment": "Team Reserve \u00e2\u20ac\u201d 420,000,000 AERE (15% of supply, team allocations)"
},
"0x261913fA73D6F109382F1aE98Ff6822ff03628B1": {
"balance": "0x73ce27351811f40c000000",
"comment": "Airdrop Reserve \u00e2\u20ac\u201d 140,000,000 AERE (5% of supply, community airdrops)"
}
}
}

View File

@ -0,0 +1,223 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aere Network <node@aere.network>
Date: Mon, 3 Aug 2026 04:00:00 +0300
Subject: [PATCH] Aere Network: fork-gated consensus floor for the EIP-1559
base fee
Adds a real consensus minimum base fee to LondonFeeMarket, gated on a fork
block. When the block number reaches aere.basefee.floor.forkBlock, the computed
EIP-1559 base fee is clamped to max(computed, aere.basefee.floor.value).
computeBaseFee is used for BOTH block production and block validation, so this
is a genuine consensus rule and not a display trick: every node must carry the
change, and a node without it computes the old, lower base fee and rejects the
floored block.
Default forkBlock is Long.MAX_VALUE, so with no configuration the behaviour is
identical to the unmodified file and pre-fork blocks validate unchanged.
NOT ACTIVE ON AERE NETWORK CHAIN 2800. No fork block has been set on mainnet.
Arming it is a governance decision that has not been taken.
Upstream-Status: Inappropriate [Aere Network specific consensus rule]
Modified upstream files. One file touched by this patch is a modified copy of a
Hyperledger Besu source, not new work by Aere Network:
ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarket.java
Aere Network changed it, against upstream commit
d2032017bb3b8cb215a97303980a1e4a643f7180. Its original "Copyright ConsenSys AG"
header is kept unchanged, which is what Apache License 2.0 section 4(c)
requires, and this patch adds below it a separate "Modifications Copyright"
block naming exactly what was changed. That in-file block is the notice
required by Apache License 2.0 section 4(b). It travels inside the diff, so a
tree with this patch applied carries the notice whether you used git am or git
apply. This patch creates no new files.
Note that the copyright holder on this file is ConsenSys AG, not "contributors
to Hyperledger Besu" as on the files touched by patches 0001 and 0002. Both
holders are named in NOTICE, as Apache License 2.0 section 4(d) requires.
---
.../mainnet/feemarket/LondonFeeMarket.java | 48 +++++++++++++++++--
1 file changed, 45 insertions(+), 3 deletions(-)
diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarket.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarket.java
index 0ee4f74..83a639c 100644
--- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarket.java
+++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/feemarket/LondonFeeMarket.java
@@ -1,56 +1,87 @@
/*
* Copyright ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
+
+/*
+ * Modifications Copyright 2026 Aere Network.
+ *
+ * This file was changed by Aere Network: a fork-gated consensus minimum for the
+ * EIP-1559 base fee was added. Two system properties configure it
+ * (aere.basefee.floor.forkBlock, aere.basefee.floor.value), a private helper
+ * applyAereBaseFeeFloor was added, and the three return paths of computeBaseFee
+ * now pass through that helper. The default fork block is Long.MAX_VALUE, so an
+ * unconfigured node behaves exactly as the unmodified file does. Nothing upstream
+ * was removed or rewritten.
+ *
+ * The unmodified original is Hyperledger Besu commit
+ * d2032017bb3b8cb215a97303980a1e4a643f7180. The upstream copyright header above is
+ * left exactly as it was found.
+ *
+ * This notice is required by Apache License 2.0 section 4(b).
+ */
package org.hyperledger.besu.ethereum.mainnet.feemarket;
import org.hyperledger.besu.config.GenesisConfig;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.core.feemarket.TransactionPriceCalculator;
import java.util.Optional;
import org.apache.tuweni.units.bigints.UInt256s;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LondonFeeMarket implements BaseFeeMarket {
private static final Logger LOG = LoggerFactory.getLogger(LondonFeeMarket.class);
static final Wei DEFAULT_BASEFEE_INITIAL_VALUE = GenesisConfig.BASEFEE_AT_GENESIS_DEFAULT_VALUE;
static final long DEFAULT_BASEFEE_MAX_CHANGE_DENOMINATOR = 8L;
static final long DEFAULT_SLACK_COEFFICIENT = 2L;
private static final Wei DEFAULT_BASEFEE_FLOOR = Wei.of(7L);
+ // === AERE base-fee floor fork (fork-gated, system-property configured) ===
+ // A real consensus minimum base fee, not a display trick. When the running block
+ // number reaches AERE_BASEFEE_FLOOR_FORK_BLOCK, the EIP-1559 base fee is clamped to
+ // max(computed, AERE_BASEFEE_FLOOR_VALUE). Because computeBaseFee is used for BOTH
+ // block production AND block validation, this is a genuine consensus rule: every node
+ // must agree, and a node without this change computes the old (lower) base fee and
+ // rejects the floored block. Default forkBlock = Long.MAX_VALUE => never active =>
+ // byte-identical behavior to stock Besu (pre-fork blocks are unchanged).
+ private static final long AERE_BASEFEE_FLOOR_FORK_BLOCK =
+ Long.getLong("aere.basefee.floor.forkBlock", Long.MAX_VALUE);
+ private static final Wei AERE_BASEFEE_FLOOR_VALUE =
+ Wei.of(Long.getLong("aere.basefee.floor.value", 1_000_000_000L));
+
protected final Wei baseFeeInitialValue;
private final long londonForkBlockNumber;
private final TransactionPriceCalculator txPriceCalculator;
private final Wei baseFeeFloor;
LondonFeeMarket(final long londonForkBlockNumber, final Optional<Wei> baseFeePerGasOverride) {
this(TransactionPriceCalculator.eip1559(), londonForkBlockNumber, baseFeePerGasOverride);
}
LondonFeeMarket(
final TransactionPriceCalculator txPriceCalculator,
final long londonForkBlockNumber,
final Optional<Wei> baseFeePerGasOverride) {
this.txPriceCalculator = txPriceCalculator;
this.londonForkBlockNumber = londonForkBlockNumber;
this.baseFeeInitialValue = baseFeePerGasOverride.orElse(DEFAULT_BASEFEE_INITIAL_VALUE);
this.baseFeeFloor = baseFeeInitialValue.isZero() ? Wei.ZERO : DEFAULT_BASEFEE_FLOOR;
}
@Override
@@ -66,72 +97,83 @@ public class LondonFeeMarket implements BaseFeeMarket {
@Override
public long getSlackCoefficient() {
return DEFAULT_SLACK_COEFFICIENT;
}
@Override
public TransactionPriceCalculator getTransactionPriceCalculator() {
return txPriceCalculator;
}
@Override
public boolean satisfiesFloorTxFee(final Transaction txn) {
// ensure effective baseFee is at least above floor
return txn.getGasPrice()
.map(Optional::of)
.orElse(txn.getMaxFeePerGas())
.filter(fee -> fee.greaterOrEqualThan(baseFeeFloor))
.isPresent();
}
+ // AERE base-fee floor fork: clamp the computed base fee to the floor once the fork
+ // block is reached. No-op before the fork block (and always a no-op when the fork is
+ // unset, i.e. forkBlock = Long.MAX_VALUE), so pre-fork blocks validate byte-identically.
+ private Wei applyAereBaseFeeFloor(final long blockNumber, final Wei fee) {
+ if (blockNumber >= AERE_BASEFEE_FLOOR_FORK_BLOCK
+ && !fee.greaterOrEqualThan(AERE_BASEFEE_FLOOR_VALUE)) {
+ return AERE_BASEFEE_FLOOR_VALUE;
+ }
+ return fee;
+ }
+
@Override
public Wei computeBaseFee(
final long blockNumber,
final Wei parentBaseFee,
final long parentBlockGasUsed,
final long targetGasUsed) {
if (londonForkBlockNumber == blockNumber) {
- return getInitialBasefee();
+ return applyAereBaseFeeFloor(blockNumber, getInitialBasefee());
}
long gasDelta;
Wei feeDelta, baseFee;
if (parentBlockGasUsed == targetGasUsed) {
- return parentBaseFee;
+ return applyAereBaseFeeFloor(blockNumber, parentBaseFee);
} else if (parentBlockGasUsed > targetGasUsed) {
gasDelta = parentBlockGasUsed - targetGasUsed;
final long denominator = getBasefeeMaxChangeDenominator();
feeDelta =
UInt256s.max(
parentBaseFee.multiply(gasDelta).divide(targetGasUsed).divide(denominator), Wei.ONE);
baseFee = parentBaseFee.add(feeDelta);
} else {
gasDelta = targetGasUsed - parentBlockGasUsed;
final long denominator = getBasefeeMaxChangeDenominator();
feeDelta = parentBaseFee.multiply(gasDelta).divide(targetGasUsed).divide(denominator);
baseFee = parentBaseFee.subtract(feeDelta);
}
LOG.trace(
"block #{} parentBaseFee: {} parentGasUsed: {} parentGasTarget: {} baseFee: {}",
blockNumber,
parentBaseFee,
parentBlockGasUsed,
targetGasUsed,
baseFee);
- return baseFee;
+ return applyAereBaseFeeFloor(blockNumber, baseFee);
}
@Override
public ValidationMode baseFeeValidationMode(final long blockNumber) {
return londonForkBlockNumber == blockNumber ? ValidationMode.INITIAL : ValidationMode.ONGOING;
}
@Override
public ValidationMode gasLimitValidationMode(final long blockNumber) {
return londonForkBlockNumber == blockNumber ? ValidationMode.INITIAL : ValidationMode.ONGOING;
}
@Override
public boolean isBeforeForkBlock(final long blockNumber) {
return londonForkBlockNumber > blockNumber;
}
}