diff --git a/CITATIONS-UNRESOLVED.md b/CITATIONS-UNRESOLVED.md new file mode 100644 index 0000000..35322dd --- /dev/null +++ b/CITATIONS-UNRESOLVED.md @@ -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 diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000..cc44c41 --- /dev/null +++ b/LICENSING.md @@ -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. diff --git a/RUN-A-NODE.md b/RUN-A-NODE.md new file mode 100644 index 0000000..4c04533 --- /dev/null +++ b/RUN-A-NODE.md @@ -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= \ + --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. diff --git a/genesis.json b/genesis.json new file mode 100644 index 0000000..efcf0f8 --- /dev/null +++ b/genesis.json @@ -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)" + } + } +} \ No newline at end of file diff --git a/patches/0004-aere-basefee-floor.patch b/patches/0004-aere-basefee-floor.patch new file mode 100644 index 0000000..2705508 --- /dev/null +++ b/patches/0004-aere-basefee-floor.patch @@ -0,0 +1,223 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: 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 baseFeePerGasOverride) { + this(TransactionPriceCalculator.eip1559(), londonForkBlockNumber, baseFeePerGasOverride); + } + + LondonFeeMarket( + final TransactionPriceCalculator txPriceCalculator, + final long londonForkBlockNumber, + final Optional 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; + } + }