anchor: the propagation adoption gate (rule C, D-412) is OBSERVE by default - it computes the same verdict and adopts, logging 'would NOT adopt'; ENFORCE only with aere.d412.adoptionGate=enforce. On the public testnet the enforcing form froze a validator that had missed one height's commit messages (D-419). Adds PqTipAdoptionGateModeTest. LICENSING.md regenerated over the 209 files the package carries.

This commit is contained in:
Aere Network 2026-09-27 20:35:53 +03:00
parent 71bce697d1
commit 93f9d5839b
5 changed files with 302 additions and 18 deletions

View File

@ -123,7 +123,7 @@ the section 4(b) notice of changed files), and each keeps its original notice pe
<!-- GENERAT: lista de mai jos se rescrie de unealta de licentiere din depozitul de constructie --> <!-- GENERAT: lista de mai jos se rescrie de unealta de licentiere din depozitul de constructie -->
## anchor/: every file that carries someone else's copyright (generated 2026-09-24) ## anchor/: every file that carries someone else's copyright (generated 2026-09-27)
This list is **generated from the files this package actually ships**, not written by hand: the This list is **generated from the files this package actually ships**, not written by hand: the
previous hand-written table fell behind the tree on 2026-09-12 and seven upstream files went previous hand-written table fell behind the tree on 2026-09-12 and seven upstream files went
@ -132,8 +132,8 @@ repository, and the publisher refuses to ship a package whose list disagrees wit
You do not need that tool to check the list: every row below names a file in this package, and You do not need that tool to check the list: every row below names a file in this package, and
`grep -i copyright` on it shows the same line. `grep -i copyright` on it shows the same line.
Measured 2026-09-24 over `anchor/`: **202 files**. **90** carry someone Measured 2026-09-27 over `anchor/`: **209 files**. **90** carry someone
else's copyright alone, **57** carry a shared notice naming both upstream and us, and **11** name only else's copyright alone, **64** carry a shared notice naming both upstream and us, and **11** name only
`contributors to the AERE Network`; 44 carry no copyright line at all. `contributors to the AERE Network`; 44 carry no copyright line at all.
The first two groups are the table below. A shared notice is listed too: those are precisely the The first two groups are the table below. A shared notice is listed too: those are precisely the
upstream files we changed, so section 4(b) has something to say about each of them. upstream files we changed, so section 4(b) has something to say about each of them.
@ -145,11 +145,14 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
|---|---|---| |---|---|---|
| `anchor/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java` | `Copyright ConsenSys AG.` | upstream |
| `anchor/app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java` | `Copyright ConsenSys AG.` | upstream |
| `anchor/app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/app/src/test/java/org/hyperledger/besu/controller/PqTipAdoptionGateModeTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java` | `Copyright ConsenSys AG.` | upstream |
| `anchor/consensus/common/build.gradle` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/common/build.gradle` | `Copyright ConsenSys AG.` | upstream |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinality.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTags.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AerePqHeaderSeal.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java` | `Copyright ConsenSys AG.` | upstream |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java` | `Copyright ConsenSys AG.` | upstream |
@ -179,6 +182,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/LICENSE-BouncyCastle.txt` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/LICENSE-BouncyCastle.txt` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)` | upstream |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ParallelSigning.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_FORS.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_FORS.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_XMSS.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_XMSS.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream |
@ -195,6 +199,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java` | `Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),` | upstream |
| `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java` | `Copyright contributors to Besu.` | upstream |
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityAuthorTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AerePqFinalityTagsTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java` | `Copyright contributors to Besu / Aere Network.` | upstream, **modified by Aere Network** | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java` | `Copyright contributors to Besu / Aere Network.` | upstream, **modified by Aere Network** |
@ -218,6 +223,7 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PreparePqAttachGateTest.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PreparePqAttachGateTest.java` | `Copyright contributors to Besu.` | upstream |
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/ProposalPqAttachGateTest.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/ProposalPqAttachGateTest.java` | `Copyright contributors to Besu.` | upstream |
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/RoundChangePqAttachGateTest.java` | `Copyright contributors to Besu.` | upstream | | `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/RoundChangePqAttachGateTest.java` | `Copyright contributors to Besu.` | upstream |
| `anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SlhDsaParallelSigningTest.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java` | `Copyright ConsenSys AG.` | upstream |
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java` | `Copyright ConsenSys AG.` | upstream |
| `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java` | `Copyright ConsenSys AG.` | upstream |
@ -290,3 +296,4 @@ notice (4(c)), and where we changed it the words **modified by Aere Network** ar
| `anchor/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AereStateUnavailableIsAnErrorTest.java` | `Copyright contributors to Hyperledger Besu.` | upstream | | `anchor/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AereStateUnavailableIsAnErrorTest.java` | `Copyright contributors to Hyperledger Besu.` | upstream |
| `anchor/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/AereHeadStateRetryTest.java` | `Copyright contributors to Hyperledger Besu.` | upstream | | `anchor/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/query/AereHeadStateRetryTest.java` | `Copyright contributors to Hyperledger Besu.` | upstream |
| `anchor/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadHeadersStep.java` | `Copyright ConsenSys AG.` | upstream | | `anchor/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/DownloadHeadersStep.java` | `Copyright ConsenSys AG.` | upstream |
| `anchor/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/PropagatedBlockAdoptionGate.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |

View File

@ -5,9 +5,10 @@ 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
0ce3fcfdcfa7ab17f4fd1251e9066e3e1c03a59047a01fb548f490391fe4cdb4 app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java 3676cf2ef881b81437ddec15bf6b8f8c393a5a6459f77179898703c289c9f8bc app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java
505066636bdd2a0e6908d490ad0b3341788d270320aed81dc2923572d61216a7 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java feaaa439fc80af14d766683231d46e5b2aa12aa6e651ed83f7ad8e9507fa1413 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
2de4b531aa8e2ea377e31908ada2d54e5e7b2df949f2456f2c0913a988f8dd00 app/src/test/java/org/hyperledger/besu/controller/PqTipAdoptionGateModeTest.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
68d635954ef33448a661256f50642eeeff2f7bb5874a54ffc2e64193c83fe391 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelay.java 68d635954ef33448a661256f50642eeeff2f7bb5874a54ffc2e64193c83fe391 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelay.java

View File

@ -61,11 +61,56 @@ import org.slf4j.LoggerFactory;
* cache cannot forge the proof. To clear the gate an adversary would need the commit quorum's distinct * cache cannot forge the proof. To clear the gate an adversary would need the commit quorum's distinct
* Falcon signatures over the block's commit message - which is exactly the agreement D-412 shows a single * Falcon signatures over the block's commit message - which is exactly the agreement D-412 shows a single
* key cannot produce. * key cannot produce.
*
* <p><b>OBSERVE by default, since D-419 (2026-09-27).</b> Measured on testnet 28001: validator
* aere-testnet@1 spent five seconds importing its own 3,366-transaction block, missed the commit messages of
* the next height, and this gate then refused the block the other four had decided - 226 times over 1 h 45
* min - while its own consensus climbed to round 10 and could never decide that height again, and the sync
* downloader never engaged ({@code eth_syncing=false} 4,000 blocks behind). The third bullet above was the
* premise, and it did not hold. Meanwhile the one scenario the gate was written for had been measured on
* 2026-09-26 WITHOUT the gate: honest validators holding the honest block at the tip did not adopt the forged
* one (0 of 3), because the honest block wins the race; the real D-412 exposure is readers and catch-up,
* which Rule B1 (finality tags) and the anchor bound (PqAnchorSealsRule) cover. So the gate now defaults to
* OBSERVE: it computes exactly the same verdict and logs every block it WOULD refuse ({@value
* #OBSERVE_MARKER}), and adopts it. ENFORCE ({@value #PROPERTY_MODE}=enforce or {@value #ENV_MODE}=enforce)
* keeps the old behaviour and must be asked for; it has no release condition yet, so it can wedge a validator
* that missed a height. A safe release needs evidence one key cannot forge (a certified anchor above the
* refused block), not a timeout: a proposer that stays silent and waits for the timeout is the D-412 attack.
*/ */
public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate { public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
private static final Logger LOG = LoggerFactory.getLogger(PqTipAdoptionGate.class); private static final Logger LOG = LoggerFactory.getLogger(PqTipAdoptionGate.class);
/** System property selecting the gate mode: {@code enforce} or {@code observe} (the default). */
public static final String PROPERTY_MODE = "aere.d412.adoptionGate";
/** Environment twin of {@link #PROPERTY_MODE}. */
public static final String ENV_MODE = "AERE_D412_ADOPTION_GATE";
/** Logged, in observe mode, for every propagated block the enforce mode would have refused. */
public static final String OBSERVE_MARKER = "AERE D-412 OBSERVE: would NOT adopt";
/**
* Whether the configuration asks for ENFORCE. Anything other than {@code enforce} (after trimming, any
* case), including nothing at all, is OBSERVE: enforcing must be an explicit request.
*
* @param property the system property value, possibly null
* @param env the environment value, possibly null
* @return true only for an explicit enforce
*/
static boolean enforceRequested(final String property, final String env) {
final String v = property != null && !property.isBlank() ? property : env;
if (v == null || v.isBlank()) {
return false;
}
final String m = v.trim().toLowerCase(java.util.Locale.ROOT);
if (!m.equals("enforce") && !m.equals("observe")) {
LOG.warn(
"AERE D-412: unknown adoption gate mode '{}' ({} / {}); using OBSERVE", m, PROPERTY_MODE, ENV_MODE);
}
return m.equals("enforce");
}
/** Verifies one Falcon seal over the commit message of a block; injectable for tests. */ /** Verifies one Falcon seal over the commit message of a block; injectable for tests. */
@FunctionalInterface @FunctionalInterface
public interface SealVerifier { public interface SealVerifier {
@ -86,9 +131,14 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
private final long chainId; private final long chainId;
private final PqSealCache cache; private final PqSealCache cache;
private final SealVerifier verifier; private final SealVerifier verifier;
private final boolean enforce;
private final java.util.function.LongPredicate stage5Active;
private final java.util.concurrent.atomic.AtomicLong wouldRefuse =
new java.util.concurrent.atomic.AtomicLong();
/** /**
* Production wiring. * Production wiring; the mode comes from {@link #PROPERTY_MODE} / {@link #ENV_MODE} (OBSERVE unless
* enforce is asked for).
* *
* @param validatorProvider the validator provider (the set at a height; ids from H_id, addresses below) * @param validatorProvider the validator provider (the set at a height; ids from H_id, addresses below)
* @param localEcdsaAddress this node's ECDSA address (from its node key) * @param localEcdsaAddress this node's ECDSA address (from its node key)
@ -101,7 +151,9 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
localEcdsaAddress, localEcdsaAddress,
chainId, chainId,
PqSealCache.instance(), PqSealCache.instance(),
(n, i, m, s) -> PqSignerRegistry.falconSealSupport().verifyAtHistoric(n, i, m, s)); (n, i, m, s) -> PqSignerRegistry.falconSealSupport().verifyAtHistoric(n, i, m, s),
enforceRequested(System.getProperty(PROPERTY_MODE), System.getenv(ENV_MODE)),
AerePqHeaderSeal::active);
} }
/** /**
@ -112,18 +164,43 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
* @param chainId the chain id * @param chainId the chain id
* @param cache the seal cache to read * @param cache the seal cache to read
* @param verifier the seal verifier * @param verifier the seal verifier
* @param enforce true to refuse (ENFORCE), false to log and adopt (OBSERVE)
* @param stage5Active whether a height is at or above H_hdr (production: {@link AerePqHeaderSeal#active})
*/ */
PqTipAdoptionGate( PqTipAdoptionGate(
final ValidatorProvider validatorProvider, final ValidatorProvider validatorProvider,
final Address localEcdsaAddress, final Address localEcdsaAddress,
final long chainId, final long chainId,
final PqSealCache cache, final PqSealCache cache,
final SealVerifier verifier) { final SealVerifier verifier,
final boolean enforce,
final java.util.function.LongPredicate stage5Active) {
this.validatorProvider = validatorProvider; this.validatorProvider = validatorProvider;
this.localEcdsaAddress = localEcdsaAddress; this.localEcdsaAddress = localEcdsaAddress;
this.chainId = chainId; this.chainId = chainId;
this.cache = cache; this.cache = cache;
this.verifier = verifier; this.verifier = verifier;
this.enforce = enforce;
this.stage5Active = stage5Active;
}
/**
* Whether this gate refuses (ENFORCE) or only logs (OBSERVE).
*
* @return true in ENFORCE mode
*/
public boolean isEnforcing() {
return enforce;
}
/**
* How many propagated blocks this gate judged without a post-quantum commit quorum (refused in ENFORCE,
* logged and adopted in OBSERVE).
*
* @return the count since start
*/
public long wouldRefuseCount() {
return wouldRefuse.get();
} }
@Override @Override
@ -131,7 +208,7 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
final long h = header.getNumber(); final long h = header.getNumber();
// Below H_hdr the header still carries a quorum of ECDSA committed seals, whose upstream rule // Below H_hdr the header still carries a quorum of ECDSA committed seals, whose upstream rule
// already refuses a single-key block; nothing to add here. // already refuses a single-key block; nothing to add here.
if (!AerePqHeaderSeal.active(h)) { if (!stage5Active.test(h)) {
return true; return true;
} }
final Collection<Address> validators = validatorProvider.getValidatorsForBlock(parent); final Collection<Address> validators = validatorProvider.getValidatorsForBlock(parent);
@ -172,16 +249,30 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
} }
} }
final boolean ok = verified.size() >= quorum; final boolean ok = verified.size() >= quorum;
if (!ok) { if (ok) {
return true;
}
wouldRefuse.incrementAndGet();
if (!enforce) {
// D-419: OBSERVE. Same verdict, recorded, and the block is adopted as before the gate existed.
LOG.info( LOG.info(
"AERE D-412: propagated block {} at tip height {} holds {} verified post-quantum commit " "{} propagated block {} at tip height {}: it holds {} verified post-quantum commit seal(s), "
+ "seal(s), quorum is {}; NOT adopting it via propagation - this node's own consensus " + "quorum is {}; adopting it (observe mode, D-419).",
+ "decides this height.", OBSERVE_MARKER,
header.getHash(), header.getHash(),
h, h,
verified.size(), verified.size(),
quorum); quorum);
return true;
} }
return ok; LOG.info(
"AERE D-412: propagated block {} at tip height {} holds {} verified post-quantum commit "
+ "seal(s), quorum is {}; NOT adopting it via propagation - this node's own consensus "
+ "decides this height.",
header.getHash(),
h,
verified.size(),
quorum);
return false;
} }
} }

View File

@ -619,12 +619,17 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder {
// ADOPTION-at-the-tip is gated, only for a validator, only from H_hdr. Inert here on any chain whose // ADOPTION-at-the-tip is gated, only for a validator, only from H_hdr. Inert here on any chain whose
// H_hdr is unset, which is chain 2800 today. Placed after the validator set is resolved (localAddress // H_hdr is unset, which is chain 2800 today. Placed after the validator set is resolved (localAddress
// and validatorProvider both exist) and before the network and the QBFT machine start. // and validatorProvider both exist) and before the network and the QBFT machine start.
// D-419 (2026-09-27): the gate defaults to OBSERVE (logs every block it would refuse, adopts it); ENFORCE
// wedged a validator that missed one height's commits on testnet 28001 and must be asked for explicitly.
if (aereAnchorConfig.everActive()) { if (aereAnchorConfig.everActive()) {
PropagatedBlockAdoptionGate.install( final PqTipAdoptionGate tipGate =
new PqTipAdoptionGate(validatorProvider, localAddress, aereAnchorConfig.chainId())); new PqTipAdoptionGate(validatorProvider, localAddress, aereAnchorConfig.chainId());
PropagatedBlockAdoptionGate.install(tipGate);
LOG.info( LOG.info(
"AERE D-412: propagation adoption gate armed; a propagated block at the tip is adopted only " "AERE D-412: propagation adoption gate armed in {} mode; a propagated block at the tip {} "
+ "with a post-quantum commit quorum from H_hdr={} (unset means never on this node).", + "without a post-quantum commit quorum from H_hdr={} (unset means never on this node).",
tipGate.isEnforcing() ? "ENFORCE" : "OBSERVE",
tipGate.isEnforcing() ? "is NOT adopted" : "is adopted and logged",
AerePqHeaderSeal.headerBlock()); AerePqHeaderSeal.headerBlock());
} }

View File

@ -0,0 +1,180 @@
/*
* Copyright contributors to Besu / AERE Network.
*
* 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.controller;
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.consensus.common.bft.AerePqIdentity;
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
import org.hyperledger.besu.consensus.common.bft.PqSealCache;
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* D-419 (2026-09-27): the Rule C tip adoption gate in its two modes. OBSERVE (the default) computes the same
* verdict as ENFORCE, counts the blocks it would refuse, and adopts them; ENFORCE refuses. Every case has its
* opposite: the same block without a quorum is refused under ENFORCE and adopted under OBSERVE, and a quorum of
* seals that do NOT verify is refused under ENFORCE, so the gate verifies seals and does not merely count them.
*/
class PqTipAdoptionGateModeTest {
private static final long H_HDR = 100L;
private static final long CHAIN_ID = 28098L;
private static final Address LOCAL = Address.fromHexString("0x00000000000000000000000000000000000000a1");
private static final List<Address> SET =
List.of(
LOCAL,
Address.fromHexString("0x00000000000000000000000000000000000000a2"),
Address.fromHexString("0x00000000000000000000000000000000000000a3"),
Address.fromHexString("0x00000000000000000000000000000000000000a4"));
// a distinct timestamp per header, so every test reads its own seal-cache entry from the shared singleton
private static final AtomicLong TS = new AtomicLong(1_900_000_000L);
private ValidatorProvider provider;
@BeforeEach
void setUp() {
// no v3 identity installed: isLocal falls back to address equality, which is what this set uses
AerePqIdentity.uninstall();
provider = mock(ValidatorProvider.class);
when(provider.getValidatorsForBlock(any())).thenReturn(SET);
}
@AfterEach
void tearDown() {
AerePqIdentity.uninstall();
}
private static BlockHeader header(final long number) {
return new BlockHeaderTestFixture().number(number).timestamp(TS.incrementAndGet()).buildHeader();
}
private PqTipAdoptionGate gate(final boolean enforce, final boolean sealsVerify) {
return new PqTipAdoptionGate(
provider,
LOCAL,
CHAIN_ID,
PqSealCache.instance(),
(n, i, m, s) -> sealsVerify,
enforce,
h -> h >= H_HDR);
}
private static void recordSeals(final BlockHeader h, final int count) {
final List<FalconSeal> seals =
java.util.stream.IntStream.range(0, count)
.mapToObj(i -> new FalconSeal(i, Bytes.of(i + 1)))
.toList();
PqSealCache.instance().record(h.getNumber(), h.getHash(), seals);
}
@Test
void enforceRefusesATipBlockWithoutAQuorum() {
final BlockHeader h = header(150);
final PqTipAdoptionGate g = gate(true, true);
assertThat(g.mayAdopt(h, header(149))).isFalse();
assertThat(g.wouldRefuseCount()).isEqualTo(1);
}
@Test
void observeAdoptsTheSameBlockAndCountsIt() {
final BlockHeader h = header(150);
final PqTipAdoptionGate g = gate(false, true);
assertThat(g.mayAdopt(h, header(149))).isTrue();
assertThat(g.wouldRefuseCount()).isEqualTo(1);
}
@Test
void enforceAdoptsWithAVerifiedQuorum() {
final BlockHeader h = header(151);
recordSeals(h, 3); // quorum of 4 validators is 3
final PqTipAdoptionGate g = gate(true, true);
assertThat(g.mayAdopt(h, header(150))).isTrue();
assertThat(g.wouldRefuseCount()).isZero();
}
@Test
void enforceRefusesAQuorumOfSealsThatDoNotVerify() {
final BlockHeader h = header(152);
recordSeals(h, 4);
final PqTipAdoptionGate g = gate(true, false);
assertThat(g.mayAdopt(h, header(151))).isFalse();
assertThat(g.wouldRefuseCount()).isEqualTo(1);
}
@Test
void belowHdrNeitherModeGatesOrCounts() {
final BlockHeader h = header(H_HDR - 1);
final PqTipAdoptionGate e = gate(true, true);
final PqTipAdoptionGate o = gate(false, true);
assertThat(e.mayAdopt(h, header(H_HDR - 2))).isTrue();
assertThat(o.mayAdopt(h, header(H_HDR - 2))).isTrue();
assertThat(e.wouldRefuseCount()).isZero();
assertThat(o.wouldRefuseCount()).isZero();
}
@Test
void aNodeOutsideTheSetIsNotGatedEvenInEnforce() {
when(provider.getValidatorsForBlock(any())).thenReturn(SET.subList(1, 4));
final PqTipAdoptionGate g = gate(true, true);
assertThat(g.mayAdopt(header(153), header(152))).isTrue();
assertThat(g.wouldRefuseCount()).isZero();
}
@Test
void enforceMustBeAskedForExplicitly() {
assertThat(PqTipAdoptionGate.enforceRequested(null, null)).isFalse();
assertThat(PqTipAdoptionGate.enforceRequested("", " ")).isFalse();
assertThat(PqTipAdoptionGate.enforceRequested("observe", null)).isFalse();
assertThat(PqTipAdoptionGate.enforceRequested("bogus", null)).isFalse();
assertThat(PqTipAdoptionGate.enforceRequested("enforce", null)).isTrue();
assertThat(PqTipAdoptionGate.enforceRequested(" ENFORCE ", null)).isTrue();
assertThat(PqTipAdoptionGate.enforceRequested(null, "enforce")).isTrue();
// the system property wins over the environment when both are set
assertThat(PqTipAdoptionGate.enforceRequested("observe", "enforce")).isFalse();
}
@Test
void theProductionConstructorDefaultsToObserve() {
final String before = System.getProperty(PqTipAdoptionGate.PROPERTY_MODE);
try {
System.clearProperty(PqTipAdoptionGate.PROPERTY_MODE);
if (System.getenv(PqTipAdoptionGate.ENV_MODE) == null) {
assertThat(new PqTipAdoptionGate(provider, LOCAL, CHAIN_ID).isEnforcing()).isFalse();
}
System.setProperty(PqTipAdoptionGate.PROPERTY_MODE, "enforce");
assertThat(new PqTipAdoptionGate(provider, LOCAL, CHAIN_ID).isEnforcing()).isTrue();
} finally {
if (before == null) {
System.clearProperty(PqTipAdoptionGate.PROPERTY_MODE);
} else {
System.setProperty(PqTipAdoptionGate.PROPERTY_MODE, before);
}
}
}
}