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 -->
## 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** |

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
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

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
* 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;
}
}

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
// 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());
}

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);
}
}
}
}