bootnodes: the second published address is withdrawn since 2026-09-11 (its host no longer fits the rule that a bootnode is not a validator; a replacement public read host follows); anchor corpus re-staged from the overlay (read-path classes and their tests, controller import-time retry)

This commit is contained in:
Aere Network 2026-09-11 15:00:59 +03:00
parent a2d2db9693
commit 7d40356aeb
12 changed files with 2068 additions and 25 deletions

View File

@ -49,7 +49,7 @@ audit each step rather than run it.
|---|---|
| `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 | two published in the bootnodes section below; run-node.sh uses them by default |
| 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 |
The registries are public (Falcon public keys and validator addresses, each carrying a proof of
@ -292,29 +292,27 @@ besu \
--min-gas-price=0
```
Two bootnodes are published here since 2026-08-25. Both are public read hosts that are NOT in
the validator set, so taking them down cannot cost the network consensus; their addresses were
already public through the services they serve. The first runs Besu, the second runs Nethermind,
so your first two peers are also two independent implementations:
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
enode://a4b90d4f5fc7758814d41acfe00f6a2516683f3d3052410091b894d014dbb2184d6158da76f72e84ca268c2ccde3590d5686f978868f3b6ad6af87e398179fb8@157.180.67.185:30303
```
`run-node.sh` uses both by default when `--bootnode` is not given. A written address is not a
live address: only your own `net_peerCount` proves they are up. If both are unreachable, write
`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: the second bootnode currently accepts connections only from its own configured
peers, so in practice your first peer is the first bootnode. A node that cannot receive inbound
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).
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,
@ -377,15 +375,15 @@ the two.
---
## What the two bootnodes are, and what they are not
## What the bootnode is, and what it is not
They are two full nodes that are **not** in the validator set. Chain 2800 seals blocks with nine
QBFT validators (seven until 2026-08-09), and none of the nine is listed here, on purpose. Publishing a validator's p2p
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 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.
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 first bootnode (37.27.216.110) now also serves the devp2p
**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,
@ -395,7 +393,7 @@ a complete stranger's snap sync to head against it has not yet been run, and the
`run-node.sh` path deliberately stays `--sync-mode=FULL`, because a validating node that replayed
every block is a stronger statement than one that trusted a pivot.
A written address is not a live address. This file cannot prove the bootnodes are up at the moment
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

View File

@ -182,7 +182,7 @@ Aere adds, changes, or that a follower must check.
- Validator set changes are made by the standard QBFT voting mechanism (the vote element of
extraData, section 2.3). The set had 3 validators at genesis (source: `genesis.json`), and the
live chain seals with ten validators since 2026-09-11 (nine from 2026-08-09, seven until then) (source: `RUN-A-NODE.md`, "What the
two bootnodes are"). The block-by-block vote history is on the chain itself and is not restated
bootnode is"). The block-by-block vote history is on the chain itself and is not restated
here.
### 2.2 Quorum

View File

@ -5,7 +5,7 @@ computed them, and the same 276 files once produced two different aggregates her
329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
05765f25da67f9b14d490121e73536a41c107edcc841c7303afaae93061ebf39 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
e17f5b548f5a0833aef11148f561f2854dacf501bbe7c98f926349707ef77429 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
129493150c0b13e8020bdf50c0c1ae07268d4f73a69a821b303bf4894451fa5e app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java
a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle
@ -170,4 +170,11 @@ a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft
321cb7e6923f77e078b523316650ac49136cf8d4df506b9bb235a32795c69611 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java
484bc74ff3d1e52d25631264d83af70c2b695e973c7360b0f3db471ab5c6613a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.java
49c3fd97e7985966531928af173b6f38ac48ff9dc8b5be1106e5b7bfb9c7b2b6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java
7ea99f24bc1c9131e8ef08f7a2c6f2a08af71ebee011a4cb88a367bd446b6c27 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetBalance.java
5bf41ab0be06328fa1e18bf9c98bfd89b7b16384fe185d96b3385595dabe61b3 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetCode.java
4c96b06073ff3830f4e0292fc732ec3fb9e0ed50ef741dceedf755b50f18d5ae ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetStorageAt.java
8e649d2a1c47cd912f0782408da3f3af5537e8c670c12cba72dbb502809c848d ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetTransactionCount.java
41b5f7b0066ef6d8af25491814eeacf2bcbb29415f407020affde05b7d1916b7 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java
3ee087fc34b455e113ec9d2c53b5dbdb3574429fcf166f4c968a80120f70f0eb ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AereStateUnavailableIsAnErrorTest.java
6e99e9f399558abbd1d4fdb4b176ba413bf31cabb3c8aba1701c17ce5828ce8a ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/AereHeadStateRetryTest.java
3b690b72e80a0f9eaf868a3fa3f99ace42dad74dc30b2c98b9f3ca5a023917c0 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadHeadersStep.java

View File

@ -318,6 +318,69 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder {
+ "opens again.",
e.toString());
}
// AERE-D358 (2026-09-10, measured on validator 8 of chain 2800): the "import path" named above
// is FalconSealValidationRule, and on 2800 that rule is RETIRED from the anchor block
// (PqAnchorConfig.legacyFalconRuleRetirementBlock), so it never runs. The startup attempt
// above is therefore the ONLY attempt, and it is silent when the head world state is not yet
// readable at that instant: after a plain container restart at 15:41Z the manifest loaded as
// PENDING, no STARTUP line was logged at all, and the node stayed PENDING for 55 minutes -
// refusing every peer Commit and Prepare as "NO post-quantum seal", emitting nothing, timing
// out its proposer slot every rotation, and slowing the whole chain from 0.56 to 1.07 s/block.
// A second restart activated at once, which is exactly what a race looks like.
//
// The repair: while the late anchor is PENDING, retry the same read (same contract, same
// slot 0, same activateLateAnchor) on every block added to the chain, and stop retrying once
// it is activated or terminally failed. It invents nothing and weakens no check.
if (FalconSealSupport.instance().anchorAddress() != null
&& FalconSealSupport.instance().lateAnchorPending()) {
final java.util.concurrent.atomic.AtomicLong aereD358Retries =
new java.util.concurrent.atomic.AtomicLong();
blockchain.observeBlockAdded(
event -> {
final FalconSealSupport pqc = FalconSealSupport.instance();
if (!pqc.lateAnchorPending()) {
return;
}
try {
final BlockHeader h = event.getBlock().getHeader();
protocolContext
.getWorldStateArchive()
.get(h.getStateRoot(), h.getHash())
.ifPresent(
ws -> {
final var cont =
ws.get(Address.fromHexString(pqc.anchorAddress()));
if (cont == null) {
return;
}
final var slot0 =
cont.getStorageValue(
org.apache.tuweni.units.bigints.UInt256.ZERO);
if (slot0 == null || slot0.isZero()) {
return;
}
final long n = aereD358Retries.incrementAndGet();
final boolean armat =
pqc.activateLateAnchor(slot0.toBytes().toUnprefixedHexString());
if (armat || n == 1 || n % 100 == 0) {
LOG.info(
"AERE-D358: late-anchor activation retried at block {} (attempt {}): {}",
h.getNumber(),
n,
armat
? "SUCCEEDED"
: "still pending (contract present, hash mismatch or refused)");
}
});
} catch (final Exception e) {
LOG.debug("AERE-D358: retry could not read the head state ({})", e.toString());
}
});
LOG.info(
"AERE-D358: late anchor is PENDING after the startup attempt; activation will be "
+ "retried on every imported block until it succeeds or terminally fails.");
}
}
// AERE A8 (2026-08-01): bind the Falcon registry to consensus.

View File

@ -0,0 +1,80 @@
/*
* 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
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import java.util.function.Supplier;
public class EthGetBalance extends AbstractBlockParameterOrBlockHashMethod {
public EthGetBalance(final BlockchainQueries blockchainQueries) {
super(blockchainQueries);
}
public EthGetBalance(final Supplier<BlockchainQueries> blockchainQueries) {
super(blockchainQueries);
}
@Override
public String getName() {
return RpcMethod.ETH_GET_BALANCE.getMethodName();
}
@Override
protected BlockParameterOrBlockHash blockParameterOrBlockHash(
final JsonRpcRequestContext request) {
try {
return request.getRequiredParameter(1, BlockParameterOrBlockHash.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid block or block hash parameter (index 1)", RpcErrorType.INVALID_BLOCK_PARAMS, e);
}
}
// AERE D-FAZA2-04 (2026-09-10): an EMPTY answer from the state query means the world state for that
// block is not available (outside the node's state window, or not yet persisted); it never means
// "no such account" - fromAccount() fills a default for that. Upstream serialised the empty answer as
// a bare JSON null, which every client reads as a value. Here it is the same honest error that
// eth_getProof already returns: WORLD_STATE_UNAVAILABLE.
@Override
protected Object resultByBlockHash(final JsonRpcRequestContext request, final Hash blockHash) {
final Address address;
try {
address = request.getRequiredParameter(0, Address.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e);
}
return blockchainQueries
.get()
.accountBalance(address, blockHash)
.map(Quantity::create)
.<Object>map(s -> s)
.orElseGet(
() ->
new JsonRpcErrorResponse(
request.getRequest().getId(), RpcErrorType.WORLD_STATE_UNAVAILABLE));
}
}

View File

@ -0,0 +1,81 @@
/*
* 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
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import java.util.function.Supplier;
import org.apache.tuweni.bytes.Bytes;
public class EthGetCode extends AbstractBlockParameterOrBlockHashMethod {
public EthGetCode(final BlockchainQueries blockchainQueries) {
super(blockchainQueries);
}
public EthGetCode(final Supplier<BlockchainQueries> blockchainQueries) {
super(blockchainQueries);
}
@Override
public String getName() {
return RpcMethod.ETH_GET_CODE.getMethodName();
}
@Override
protected BlockParameterOrBlockHash blockParameterOrBlockHash(
final JsonRpcRequestContext request) {
try {
return request.getRequiredParameter(1, BlockParameterOrBlockHash.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid block or block hash parameter (index 1)", RpcErrorType.INVALID_BLOCK_PARAMS, e);
}
}
// AERE D-FAZA2-04 (2026-09-10): an EMPTY answer from the state query means the world state for that
// block is not available (outside the node's state window, or not yet persisted); it never means
// "no such account" - fromAccount() fills a default for that. Upstream serialised the empty answer as
// a bare JSON null, which every client reads as a value. Here it is the same honest error that
// eth_getProof already returns: WORLD_STATE_UNAVAILABLE.
@Override
protected Object resultByBlockHash(final JsonRpcRequestContext request, final Hash blockHash) {
final Address address;
try {
address = request.getRequiredParameter(0, Address.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e);
}
return getBlockchainQueries()
.getCode(address, blockHash)
.map(Bytes::toString)
.<Object>map(s -> s)
.orElseGet(
() ->
new JsonRpcErrorResponse(
request.getRequest().getId(), RpcErrorType.WORLD_STATE_UNAVAILABLE));
}
}

View File

@ -0,0 +1,83 @@
/*
* 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
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.parameters.UInt256Parameter;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.apache.tuweni.units.bigints.UInt256;
public class EthGetStorageAt extends AbstractBlockParameterOrBlockHashMethod {
public EthGetStorageAt(final BlockchainQueries blockchainQueries) {
super(blockchainQueries);
}
@Override
public String getName() {
return RpcMethod.ETH_GET_STORAGE_AT.getMethodName();
}
@Override
protected BlockParameterOrBlockHash blockParameterOrBlockHash(
final JsonRpcRequestContext request) {
try {
return request.getRequiredParameter(2, BlockParameterOrBlockHash.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid block or block hash parameter (index 2)", RpcErrorType.INVALID_BLOCK_PARAMS, e);
}
}
@Override
// AERE D-FAZA2-04 (2026-09-10): an EMPTY answer from the state query means the world state for that
// block is not available (outside the node's state window, or not yet persisted); it never means
// "no such account" - fromAccount() fills a default for that. Upstream serialised the empty answer as
// a bare JSON null, which every client reads as a value. Here it is the same honest error that
// eth_getProof already returns: WORLD_STATE_UNAVAILABLE.
protected Object resultByBlockHash(final JsonRpcRequestContext request, final Hash blockHash) {
final Address address;
try {
address = request.getRequiredParameter(0, Address.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e);
}
final UInt256 position;
try {
position = request.getRequiredParameter(1, UInt256Parameter.class).getValue();
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid position parameter (index 1)", RpcErrorType.INVALID_POSITION_PARAMS, e);
}
return blockchainQueries
.get()
.storageAt(address, position, blockHash)
.map(UInt256::toHexString)
.<Object>map(s -> s)
.orElseGet(
() ->
new JsonRpcErrorResponse(
request.getRequest().getId(), RpcErrorType.WORLD_STATE_UNAVAILABLE));
}
}

View File

@ -0,0 +1,113 @@
/*
* 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
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.BlockParameterOrBlockHash;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter.JsonRpcParameterException;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.Quantity;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import java.util.function.Supplier;
import com.google.common.base.Suppliers;
public class EthGetTransactionCount extends AbstractBlockParameterOrBlockHashMethod {
private final Supplier<TransactionPool> transactionPoolSupplier;
public EthGetTransactionCount(
final BlockchainQueries blockchain, final TransactionPool transactionPoolSupplier) {
this(Suppliers.ofInstance(blockchain), Suppliers.ofInstance(transactionPoolSupplier));
}
public EthGetTransactionCount(
final Supplier<BlockchainQueries> blockchain,
final Supplier<TransactionPool> transactionPoolSupplier) {
super(blockchain);
this.transactionPoolSupplier = transactionPoolSupplier;
}
@Override
public String getName() {
return RpcMethod.ETH_GET_TRANSACTION_COUNT.getMethodName();
}
@Override
protected BlockParameterOrBlockHash blockParameterOrBlockHash(
final JsonRpcRequestContext request) {
try {
return request.getRequiredParameter(1, BlockParameterOrBlockHash.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid block or block hash parameter (index 1)", RpcErrorType.INVALID_BLOCK_PARAMS, e);
}
}
@Override
protected Object pendingResult(final JsonRpcRequestContext request) {
final Address address;
try {
address = request.getRequiredParameter(0, Address.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e);
}
final long pendingNonce =
transactionPoolSupplier.get().getNextNonceForSender(address).orElse(0);
final long latestNonce =
getBlockchainQueries()
.getTransactionCount(
address, getBlockchainQueries().getBlockchain().getChainHead().getHash());
if (Long.compareUnsigned(pendingNonce, latestNonce) > 0) {
return Quantity.create(pendingNonce);
}
return Quantity.create(latestNonce);
}
// AERE D-FAZA2-04 (2026-09-10): an EMPTY answer from the state query means the world state for that
// block is not available (outside the node's state window, or not yet persisted); it never means
// "no such account" - fromAccount() fills a default for that. Upstream serialised the empty answer as
// a bare JSON null, which every client reads as a value. Here it is the same honest error that
// eth_getProof already returns: WORLD_STATE_UNAVAILABLE.
// Upstream folded the missing state into a nonce of 0 (`.orElse(0L)`), a FALSE 0x0 that a wallet
// signs with. Here the nonce is asked for as an Optional and the missing state is an error.
@Override
protected Object resultByBlockHash(final JsonRpcRequestContext request, final Hash blockHash) {
final Address address;
try {
address = request.getRequiredParameter(0, Address.class);
} catch (JsonRpcParameterException e) {
throw new InvalidJsonRpcParameters(
"Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e);
}
return getBlockchainQueries()
.getTransactionCountIfStateAvailable(address, blockHash)
.map(Quantity::create)
.<Object>map(s -> s)
.orElseGet(
() ->
new JsonRpcErrorResponse(
request.getRequest().getId(), RpcErrorType.WORLD_STATE_UNAVAILABLE));
}
}

View File

@ -0,0 +1,138 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* 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
*/
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
import org.hyperledger.besu.ethereum.api.query.BlockchainQueries;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
import java.util.Optional;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.units.bigints.UInt256;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* AERE D-FAZA2-04: when the world state for the requested block is not available, the four state
* methods answer with the WORLD_STATE_UNAVAILABLE error, never with a bare null (or a nonce of 0).
* When the state IS available they answer as before.
*/
public class AereStateUnavailableIsAnErrorTest {
private static final Address ADDR = Address.fromHexString("0x1111111111111111111111111111111111111111");
private static final Hash BLOCK_HASH = Hash.fromHexStringLenient("0xabcd");
private static final long HEAD = 1000L;
private BlockchainQueries queries;
@BeforeEach
public void setUp() {
queries = mock(BlockchainQueries.class);
// the numeric block parameter path: number -> hash
when(queries.headBlockNumber()).thenReturn(HEAD);
when(queries.getBlockHashByNumber(HEAD)).thenReturn(Optional.of(BLOCK_HASH));
final BlockHeader header = mock(BlockHeader.class);
when(header.getBlockHash()).thenReturn(BLOCK_HASH);
when(queries.getBlockHeaderByNumber(HEAD)).thenReturn(Optional.of(header));
when(queries.getBlockHeaderByHash(BLOCK_HASH)).thenReturn(Optional.of(header));
}
private static JsonRpcRequestContext request(final String method, final Object... params) {
return new JsonRpcRequestContext(new JsonRpcRequest("2.0", method, params));
}
private static void assertWorldStateUnavailable(final JsonRpcResponse response) {
assertThat(response).isInstanceOf(JsonRpcErrorResponse.class);
assertThat(((JsonRpcErrorResponse) response).getErrorType())
.isEqualTo(RpcErrorType.WORLD_STATE_UNAVAILABLE);
}
private static Object result(final JsonRpcResponse response) {
assertThat(response).isInstanceOf(JsonRpcSuccessResponse.class);
return ((JsonRpcSuccessResponse) response).getResult();
}
@Test
public void getCodeWithoutStateIsAnError() {
when(queries.getCode(any(Address.class), any(Hash.class))).thenReturn(Optional.empty());
final EthGetCode method = new EthGetCode(queries);
assertWorldStateUnavailable(method.response(request("eth_getCode", ADDR.toHexString(), "0x3e8")));
}
@Test
public void getCodeWithStateAnswersAsBefore() {
when(queries.getCode(any(Address.class), any(Hash.class))).thenReturn(Optional.of(Bytes.fromHexString("0x6001")));
final EthGetCode method = new EthGetCode(queries);
assertThat(result(method.response(request("eth_getCode", ADDR.toHexString(), "0x3e8")))).isEqualTo("0x6001");
}
@Test
public void getBalanceWithoutStateIsAnError() {
when(queries.accountBalance(any(Address.class), any(Hash.class))).thenReturn(Optional.empty());
final EthGetBalance method = new EthGetBalance(queries);
assertWorldStateUnavailable(method.response(request("eth_getBalance", ADDR.toHexString(), "0x3e8")));
}
@Test
public void getBalanceWithStateAnswersAsBefore() {
when(queries.accountBalance(any(Address.class), any(Hash.class))).thenReturn(Optional.of(Wei.of(7)));
final EthGetBalance method = new EthGetBalance(queries);
assertThat(result(method.response(request("eth_getBalance", ADDR.toHexString(), "0x3e8")))).isEqualTo("0x7");
}
@Test
public void getStorageAtWithoutStateIsAnError() {
when(queries.storageAt(any(Address.class), any(UInt256.class), any(Hash.class))).thenReturn(Optional.empty());
final EthGetStorageAt method = new EthGetStorageAt(queries);
assertWorldStateUnavailable(method.response(request("eth_getStorageAt", ADDR.toHexString(), "0x0", "0x3e8")));
}
@Test
public void getStorageAtWithStateAnswersAsBefore() {
when(queries.storageAt(any(Address.class), any(UInt256.class), any(Hash.class))).thenReturn(Optional.of(UInt256.valueOf(5)));
final EthGetStorageAt method = new EthGetStorageAt(queries);
assertThat(result(method.response(request("eth_getStorageAt", ADDR.toHexString(), "0x0", "0x3e8"))))
.isEqualTo(UInt256.valueOf(5).toHexString());
}
@Test
public void getTransactionCountWithoutStateIsAnErrorNotZero() {
when(queries.getTransactionCountIfStateAvailable(any(Address.class), any(Hash.class))).thenReturn(Optional.empty());
final EthGetTransactionCount method = new EthGetTransactionCount(queries, mock(TransactionPool.class));
assertWorldStateUnavailable(method.response(request("eth_getTransactionCount", ADDR.toHexString(), "0x3e8")));
}
@Test
public void getTransactionCountWithStateAnswersAsBefore() {
when(queries.getTransactionCountIfStateAvailable(any(Address.class), any(Hash.class))).thenReturn(Optional.of(3L));
final EthGetTransactionCount method = new EthGetTransactionCount(queries, mock(TransactionPool.class));
assertThat(result(method.response(request("eth_getTransactionCount", ADDR.toHexString(), "0x3e8")))).isEqualTo("0x3");
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright contributors to Hyperledger Besu.
*
* 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
*/
package org.hyperledger.besu.ethereum.api.query;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.chain.Blockchain;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.MiningConfiguration;
import org.hyperledger.besu.ethereum.core.MutableWorldState;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.ethereum.trie.pathbased.common.provider.WorldStateQueryParams;
import java.util.Optional;
import org.junit.jupiter.api.Test;
/**
* AERE D-109: a query against a block that has just become the chain head can find no world state
* for a few milliseconds. {@link BlockchainQueries#getAndMapWorldState(Hash, java.util.function.Function)}
* must look again (a bounded number of times) near the head, and must NOT retry for old blocks,
* where an empty result is the state window closing.
*/
public class AereHeadStateRetryTest {
private static final Hash HASH = Hash.fromHexStringLenient("0x1234");
private static BlockHeader headerAt(final long number) {
final BlockHeader header = mock(BlockHeader.class);
when(header.getNumber()).thenReturn(number);
when(header.getHash()).thenReturn(HASH);
when(header.getBlockHash()).thenReturn(HASH);
when(header.getStateRoot()).thenReturn(Hash.EMPTY_TRIE_HASH);
when(header.getParentHash()).thenReturn(Hash.ZERO);
when(header.toLogString()).thenReturn("#" + number);
return header;
}
private static BlockchainQueries queries(
final Blockchain blockchain, final WorldStateArchive archive) {
return new BlockchainQueries(
mock(ProtocolSchedule.class), blockchain, archive, MiningConfiguration.newDefault());
}
@Test
public void headBlockStateThatAppearsOnSecondLookIsReturned() {
final Blockchain blockchain = mock(Blockchain.class);
final WorldStateArchive archive = mock(WorldStateArchive.class);
final BlockHeader head = headerAt(1000);
when(blockchain.getBlockHeader(HASH)).thenReturn(Optional.of(head));
when(blockchain.getChainHeadBlockNumber()).thenReturn(1000L);
final MutableWorldState ws = mock(MutableWorldState.class);
when(archive.getWorldState(any(WorldStateQueryParams.class)))
.thenReturn(Optional.empty())
.thenReturn(Optional.of(ws));
final Optional<String> result =
queries(blockchain, archive).getAndMapWorldState(HASH, w -> Optional.of("ok"));
assertThat(result).contains("ok");
verify(archive, times(2)).getWorldState(any(WorldStateQueryParams.class));
}
@Test
public void oldBlockWithoutStateIsNotRetried() {
// negative control: an old block gets exactly ONE look, and an empty answer stays empty
final Blockchain blockchain = mock(Blockchain.class);
final WorldStateArchive archive = mock(WorldStateArchive.class);
final BlockHeader old = headerAt(1000);
when(blockchain.getBlockHeader(HASH)).thenReturn(Optional.of(old));
when(blockchain.getChainHeadBlockNumber()).thenReturn(5000L);
when(archive.getWorldState(any(WorldStateQueryParams.class))).thenReturn(Optional.empty());
final Optional<String> result =
queries(blockchain, archive).getAndMapWorldState(HASH, w -> Optional.of("ok"));
assertThat(result).isEmpty();
verify(archive, times(1)).getWorldState(any(WorldStateQueryParams.class));
}
@Test
public void headBlockThatNeverGetsStateStopsAfterBoundedRetries() {
// the retry is bounded: a head block that never gets its state is asked 1 + 4 times, then empty
final Blockchain blockchain = mock(Blockchain.class);
final WorldStateArchive archive = mock(WorldStateArchive.class);
final BlockHeader head = headerAt(1000);
when(blockchain.getBlockHeader(HASH)).thenReturn(Optional.of(head));
when(blockchain.getChainHeadBlockNumber()).thenReturn(1001L);
when(archive.getWorldState(any(WorldStateQueryParams.class))).thenReturn(Optional.empty());
final long t0 = System.nanoTime();
final Optional<String> result =
queries(blockchain, archive).getAndMapWorldState(HASH, w -> Optional.of("ok"));
final long elapsedMs = (System.nanoTime() - t0) / 1_000_000;
assertThat(result).isEmpty();
verify(archive, times(5)).getWorldState(any(WorldStateQueryParams.class));
// 50 + 100 + 150 + 300 ms of pauses, and nothing unbounded
assertThat(elapsedMs).isBetween(550L, 5000L);
}
@Test
public void stateFoundOnFirstLookIsReturnedWithoutWaiting() {
final Blockchain blockchain = mock(Blockchain.class);
final WorldStateArchive archive = mock(WorldStateArchive.class);
final BlockHeader head = headerAt(1000);
when(blockchain.getBlockHeader(HASH)).thenReturn(Optional.of(head));
when(blockchain.getChainHeadBlockNumber()).thenReturn(1000L);
when(archive.getWorldState(any(WorldStateQueryParams.class)))
.thenReturn(Optional.of(mock(MutableWorldState.class)));
final long t0 = System.nanoTime();
final Optional<String> result =
queries(blockchain, archive).getAndMapWorldState(HASH, w -> Optional.of("ok"));
final long elapsedMs = (System.nanoTime() - t0) / 1_000_000;
assertThat(result).contains("ok");
verify(archive, times(1)).getWorldState(any(WorldStateQueryParams.class));
assertThat(elapsedMs).isLessThan(50L);
}
}

View File

@ -15,7 +15,7 @@
# Re-running it does not rebuild what is already built or re-apply patches already applied.
#
# Usage:
# ./run-node.sh [options] (the two published bootnodes are used unless --bootnode is given)
# ./run-node.sh [options] (the published bootnode is used unless --bootnode is given)
#
# Options:
# --bootnode <enode> Override the published bootnodes with your own peer for discovery.
@ -207,10 +207,10 @@ fi
# --- 6. start ----------------------------------------------------------------------------------
say "6/6 Starting the node"
if [ -z "$BOOTNODE" ]; then
# The two published bootnodes (RUN-A-NODE.md, bootnodes section): both public read hosts,
# neither in the validator set, one Besu and one Nethermind. --bootnode overrides them.
BOOTNODE="enode://7e8ff740b79bf28a6d7e46ea8d2317cea4a223e7af60be36e91e13dc7051f4cb29077631452e699a5e18e640a94fd6e370e9e51dd91ecba1f2ba9666e07ff01b@37.27.216.110:30303,enode://a4b90d4f5fc7758814d41acfe00f6a2516683f3d3052410091b894d014dbb2184d6158da76f72e84ca268c2ccde3590d5686f978868f3b6ad6af87e398179fb8@157.180.67.185:30303"
say " no --bootnode given; using the two published bootnodes from RUN-A-NODE.md"
# The published bootnode (RUN-A-NODE.md, bootnodes section): a public read host, not in the validator
# set. A second address was published from 2026-08-25 to 2026-09-11 and withdrawn. --bootnode overrides it.
BOOTNODE="enode://7e8ff740b79bf28a6d7e46ea8d2317cea4a223e7af60be36e91e13dc7051f4cb29077631452e699a5e18e640a94fd6e370e9e51dd91ecba1f2ba9666e07ff01b@37.27.216.110:30303"
say " no --bootnode given; using the published bootnode from RUN-A-NODE.md"
fi
mkdir -p "$DATA_PATH"