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:
parent
71bce697d1
commit
93f9d5839b
13
LICENSING.md
13
LICENSING.md
@ -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 -->
|
||||
|
||||
## 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
|
||||
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
|
||||
`grep -i copyright` on it shows the same line.
|
||||
|
||||
Measured 2026-09-24 over `anchor/`: **202 files**. **90** carry someone
|
||||
else's copyright alone, **57** carry a shared notice naming both upstream and us, and **11** name only
|
||||
Measured 2026-09-27 over `anchor/`: **209 files**. **90** carry someone
|
||||
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.
|
||||
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.
|
||||
@ -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/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/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/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/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/BftBlockInterface.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/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/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_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 |
|
||||
@ -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/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/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/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** |
|
||||
@ -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/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/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/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 |
|
||||
@ -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/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/PropagatedBlockAdoptionGate.java` | `Copyright contributors to Besu / AERE Network.` | upstream, **modified by Aere Network** |
|
||||
|
||||
@ -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
|
||||
3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
|
||||
0ce3fcfdcfa7ab17f4fd1251e9066e3e1c03a59047a01fb548f490391fe4cdb4 app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java
|
||||
505066636bdd2a0e6908d490ad0b3341788d270320aed81dc2923572d61216a7 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
|
||||
3676cf2ef881b81437ddec15bf6b8f8c393a5a6459f77179898703c289c9f8bc app/src/main/java/org/hyperledger/besu/controller/PqTipAdoptionGate.java
|
||||
feaaa439fc80af14d766683231d46e5b2aa12aa6e651ed83f7ad8e9507fa1413 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.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
|
||||
a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle
|
||||
68d635954ef33448a661256f50642eeeff2f7bb5874a54ffc2e64193c83fe391 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelay.java
|
||||
|
||||
@ -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
|
||||
* Falcon signatures over the block's commit message - which is exactly the agreement D-412 shows a single
|
||||
* 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 {
|
||||
|
||||
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. */
|
||||
@FunctionalInterface
|
||||
public interface SealVerifier {
|
||||
@ -86,9 +131,14 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
|
||||
private final long chainId;
|
||||
private final PqSealCache cache;
|
||||
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 localEcdsaAddress this node's ECDSA address (from its node key)
|
||||
@ -101,7 +151,9 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
|
||||
localEcdsaAddress,
|
||||
chainId,
|
||||
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 cache the seal cache to read
|
||||
* @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(
|
||||
final ValidatorProvider validatorProvider,
|
||||
final Address localEcdsaAddress,
|
||||
final long chainId,
|
||||
final PqSealCache cache,
|
||||
final SealVerifier verifier) {
|
||||
final SealVerifier verifier,
|
||||
final boolean enforce,
|
||||
final java.util.function.LongPredicate stage5Active) {
|
||||
this.validatorProvider = validatorProvider;
|
||||
this.localEcdsaAddress = localEcdsaAddress;
|
||||
this.chainId = chainId;
|
||||
this.cache = cache;
|
||||
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
|
||||
@ -131,7 +208,7 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
|
||||
final long h = header.getNumber();
|
||||
// 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.
|
||||
if (!AerePqHeaderSeal.active(h)) {
|
||||
if (!stage5Active.test(h)) {
|
||||
return true;
|
||||
}
|
||||
final Collection<Address> validators = validatorProvider.getValidatorsForBlock(parent);
|
||||
@ -172,16 +249,30 @@ public class PqTipAdoptionGate implements PropagatedBlockAdoptionGate.Gate {
|
||||
}
|
||||
}
|
||||
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(
|
||||
"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.",
|
||||
"{} propagated block {} at tip height {}: it holds {} verified post-quantum commit seal(s), "
|
||||
+ "quorum is {}; adopting it (observe mode, D-419).",
|
||||
OBSERVE_MARKER,
|
||||
header.getHash(),
|
||||
h,
|
||||
verified.size(),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
// 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.
|
||||
// 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()) {
|
||||
PropagatedBlockAdoptionGate.install(
|
||||
new PqTipAdoptionGate(validatorProvider, localAddress, aereAnchorConfig.chainId()));
|
||||
final PqTipAdoptionGate tipGate =
|
||||
new PqTipAdoptionGate(validatorProvider, localAddress, aereAnchorConfig.chainId());
|
||||
PropagatedBlockAdoptionGate.install(tipGate);
|
||||
LOG.info(
|
||||
"AERE D-412: propagation adoption gate armed; a propagated block at the tip is adopted only "
|
||||
+ "with a post-quantum commit quorum from H_hdr={} (unset means never on this node).",
|
||||
"AERE D-412: propagation adoption gate armed in {} mode; a propagated block at the tip {} "
|
||||
+ "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());
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user