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. | | `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. | | `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 | | `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 The registries are public (Falcon public keys and validator addresses, each carrying a proof of
@ -292,29 +292,27 @@ besu \
--min-gas-price=0 --min-gas-price=0
``` ```
Two bootnodes are published here since 2026-08-25. Both are public read hosts that are NOT in One bootnode is published here (two from 2026-08-25 to 2026-09-11; the second address was withdrawn on 2026-09-11
the validator set, so taking them down cannot cost the network consensus; their addresses were because its host no longer fits the rule in the section below, and a replacement public read host will be
already public through the services they serve. The first runs Besu, the second runs Nethermind, published when it exists). It is a public read host that is NOT in the validator set, so taking it down
so your first two peers are also two independent implementations: 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://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 `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 they are up. If both are unreachable, write 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. 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 Measured 2026-09-11: a node that cannot receive inbound
peers, so in practice your first peer is the first bootnode. A node that cannot receive inbound
connections (behind NAT without a forwarded port) keeps that single peer, and Besu does not fetch the 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 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. `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 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 `--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 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 `--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, 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 It is a full node that is **not** in the validator set. Chain 2800 seals blocks with ten QBFT
QBFT validators (seven until 2026-08-09), and none of the nine is listed here, on purpose. Publishing a validator's p2p 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 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 sentry style architectures exist. The node above carries no consensus responsibility: if it is
taken down, new nodes cannot bootstrap until they are replaced, and block production is unaffected. 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 `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 `[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, 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 `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. 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. 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 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 - 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 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 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. here.
### 2.2 Quorum ### 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 329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.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 129493150c0b13e8020bdf50c0c1ae07268d4f73a69a821b303bf4894451fa5e app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java 728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java
a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle 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 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 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 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 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.", + "opens again.",
e.toString()); 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. // 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. # Re-running it does not rebuild what is already built or re-apply patches already applied.
# #
# Usage: # 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: # Options:
# --bootnode <enode> Override the published bootnodes with your own peer for discovery. # --bootnode <enode> Override the published bootnodes with your own peer for discovery.
@ -207,10 +207,10 @@ fi
# --- 6. start ---------------------------------------------------------------------------------- # --- 6. start ----------------------------------------------------------------------------------
say "6/6 Starting the node" say "6/6 Starting the node"
if [ -z "$BOOTNODE" ]; then if [ -z "$BOOTNODE" ]; then
# The two published bootnodes (RUN-A-NODE.md, bootnodes section): both public read hosts, # The published bootnode (RUN-A-NODE.md, bootnodes section): a public read host, not in the validator
# neither in the validator set, one Besu and one Nethermind. --bootnode overrides them. # 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,enode://a4b90d4f5fc7758814d41acfe00f6a2516683f3d3052410091b894d014dbb2184d6158da76f72e84ca268c2ccde3590d5686f978868f3b6ad6af87e398179fb8@157.180.67.185:30303" BOOTNODE="enode://7e8ff740b79bf28a6d7e46ea8d2317cea4a223e7af60be36e91e13dc7051f4cb29077631452e699a5e18e640a94fd6e370e9e51dd91ecba1f2ba9666e07ff01b@37.27.216.110:30303"
say " no --bootnode given; using the two published bootnodes from RUN-A-NODE.md" say " no --bootnode given; using the published bootnode from RUN-A-NODE.md"
fi fi
mkdir -p "$DATA_PATH" mkdir -p "$DATA_PATH"