diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt index 9057423..6f5d727 100644 --- a/anchor/MANIFEST-sha256.txt +++ b/anchor/MANIFEST-sha256.txt @@ -38,17 +38,7 @@ addba40c0d931a3ecfa3b2f0179311dacaa604884e3ebb1d79f958bd913f72e2 consensus/comm b6c9ecbf3cd2ee73111984cd04a89c32ee56a1cc699a197b65ef7c10001b8cbc consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java 5c8861eba1ea697d8deb88139d92c6ea6636b29d76c881b13e7f6c841ba2324f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java 6ace00e18914a1558563e689b7427654b5e6226a31447bef1d0416d993f78840 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java -2620bb98485e5d5649b58d331d1a35031bef56cab8bd18d1e7f9050e61bc09de consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ThresholdReachabilityTest.java -5534cfab3bd59968823265655351491abef8ea73f594677a62bf47c8e88deb54 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ValidatorSetChangeTest.java -da249c59f356e06928f73543d5529ae1911e4613a8125477f475b1dd54daed62 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D079ForkArmingTest.java -9a794d7a4010ff5c561008229cb2ae97d5f79367641190c97cde8802b83daa72 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D081RegistryRotationTest.java -9ab9918f4e5bef6485fef1251a07504e32b8b2d42393d3fcbb35d7e1241a7deb consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D140FleetRestartArmingTest.java -2e81e02a419cdb2053b67e2bc9e699af1366955fa18d351178637b64a48a27be consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D141SealPersistenceTest.java -42dd6396583aa19475e23c9781f568958d9789f0dba084dc63ac9833203f3f1f consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D146ArmingGateTest.java -75fca200a2ac9e7226c70d96916a4ae33201a8acb466d0fdc591576e09edb3fc consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D147InertBinaryTest.java acd5c1e7f49dbbf444c19c91386bbd03b9298d952d2d13721793670103e71ee9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D177NeutralNamesTest.java -bc6b58570f835276324a5bd2704e7de8cac4e0693bd0138199d149c1ec7d4174 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D2CallerIntentTest.java -4aeab501c83f0d7fbe7a45c99e2035fd034b055b2a6cd5925026e29e4e427849 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D2RegistryHeightRefusalTest.java c574ec22e642ac464b1728d27e4bb26202448a45c044f08935cc0a0efa0297d9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FalconAttachIntervalTest.java 0eb20506f851c1510bcaf659db8c5b0d384cdc60ed06192a03a665f0ee2b3912 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducerTest.java 186c4c92199400783ee3424231818dd440b0da41aeb3d8bd643cd13759998250 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupportTest.java @@ -58,7 +48,7 @@ c574ec22e642ac464b1728d27e4bb26202448a45c044f08935cc0a0efa0297d9 consensus/comm 7470d72271dd4f9de96094b3d1c6dc4b5b5ea62e46a058d4e154e3742d3ccef1 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java e2487ff508ffb51bb61ca19531f4c4e68c5981611fd9fec5c92a2965f0bb8f47 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java dabb059478da5259c309e6637e987ab35ef7b69cc286e41f936e4f3e83d3f082 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java -c20743ce1b54af2b7b9d42366bef14678fd94aa8d1dc98eabf21ff0f9ae15302 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCacheHygieneTest.java +fe84c239e3504f55e8d950e53b844641cc1dd7a7c864dd8f58db3d2f50c12b9d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCacheHygieneTest.java 0bf1c8a9cdd91d7c34053d6e5b4ff929787f34eb7aa7d6eed26fc5c94fa1c42b consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCostTest.java 23daeb4888c8de8337518a27ef60e42573ebafaf340075e8ce9236f0d8d580f6 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java 09c41c77408acc4711e4c46175485bf41babff0d0a8ec7481670fddfd28967b0 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java @@ -136,8 +126,6 @@ ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft 8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java 52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java 5a18c7fee308654d9557fc507ca7a64704bdac5d13835e7df47a3dfb41519902 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRuleTest.java -8f27193a286d1e6bb4c84f98e5af9821ee9a22873a26288fad67256c710a51cd consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D078GateFeedTest.java -ebc5811c3a765b1175023d2c767eb8c71f4f2bdbb63ec0b354294a3f0ac15dfa consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D079ArmedWithoutRegistryTest.java ad018cba0a3fe7f018b11c6c6a2d45e3f5547342ec45b620a4df2595801ae71a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealLogThrottleTest.java 3895d10bcf5ffbdaf0506503a0d9e3d72c600288def268fd5e9f68c4a042e162 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRuleRetirementTest.java e2df575ee4d6ab5bd961b0886ece3d3c392a50193f1d3256a438a72da9e20d10 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRuleTest.java diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ThresholdReachabilityTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ThresholdReachabilityTest.java deleted file mode 100644 index c64af47..0000000 --- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ThresholdReachabilityTest.java +++ /dev/null @@ -1,356 +0,0 @@ -/* - * 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.consensus.common.bft; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - - -import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer; - -import java.lang.reflect.Field; -import java.nio.file.Files; -import java.nio.file.Path; - -import org.apache.tuweni.bytes.Bytes; -import org.bouncycastle.crypto.digests.KeccakDigest; -import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -/** - * D-078, THE HALF THAT WAS STILL OPEN: is the threshold K one the fleet can be GUARANTEED to meet? - * - *
The 2026-08-02 repair closed the mechanism that stopped the chain on one add-validator vote: it - * took the fleet-wide coverage question out of the per-commit attachment gate and made coverage a - * REPORT. That repair is correct and it is measured next door in {@code D078ValidatorSetChangeTest}. - * But it left behind an explicit promise, written in the javadoc of {@code attachmentArmed}: - * - *
- * - * "What coverage genuinely protects - that blocking is not ARMED over a partial manifest - is an - * arm-time decision, and it is made at arm time by armingReadinessDiagnostic() and by the operator". - * - *- * - *
MEASURED 2026-08-03: {@code armingReadinessDiagnostic()} checks exactly one thing, whether the - * manifest is ADDRESS-BOUND. It never reads the fleet size, never reads how many validators hold an - * anchored key, and never reads K. The arm-time decision the comment names did not exist, so the - * compensating control for the repair was a sentence. This class is what makes it exist. - * - *
THE ARITHMETIC, which is the whole finding and is not an opinion. A block needs {@code - * ceil(2N/3)} ECDSA committers, and Falcon seals ride on Commit messages, so the seals a proposer is - * GUARANTEED to hear are only those of the keyed validators it cannot avoid: {@code quorum - (N - - * keyed)}. The row that matters for this project: - * - *
- * N=7, keyed 7, quorum 5 -> 5 guaranteed K=5 reachable, margin exactly 0 - * N=9, keyed 7, quorum 6 -> 4 guaranteed K=5 NOT guaranteed - *- * - *
The second row is the standing plan. "Grow to N=9 BEFORE arming" is right, and if the manifest - * is not re-anchored on the way there it produces a fleet that arms a threshold no proposer is - * guaranteed to meet. Before this guard a node in that state started, joined, armed, and the failure - * appeared later as a proposer that could not propose. That is the most expensive shape a - * configuration error can take, and it is the same shape the A8 repair already refused to allow for - * a non-address-bound manifest. - * - *
WHY AT CONFIG TIME AND NOWHERE ELSE. The lesson is borrowed, not invented: CometBFT applies a - * validator-set change only at H+2 and Ethereum's light-client protocol carries {@code - * next_sync_committee} a whole period ahead, both so that the set a cryptographic check runs over is - * known and comparable BEFORE the boundary rather than discovered at it. We cannot copy their - * mechanism, because at seven nodes under one operator there is no committee to sample. We can copy - * the discipline: DECLARE the fleet size, compare it against the threshold at config time, and - * refuse to cross the boundary if the comparison fails. The same reasoning already produced - * AERE-PQC-CFG-UNSAFE-04 and, for the fork height, AERE-PQC-CFG-UNSAFE-06/07 in D-079. - * - *
NOT MEASURED here, and named so it is not read as covered: what a LIVE fleet does in the rounds - * between the vote landing and the first proposer failing. That needs a network. This class measures - * the decision, which is the thing a node can be stopped from taking. - */ -public class D078ThresholdReachabilityTest { - - /** Anchor activation height H. */ - private static final long H = 1_000L; - - /** The height from which the staged threshold is K. */ - private static final long K_AT = H + 10L; - - /** The threshold this project intends to arm. */ - private static final int K = 5; - - /** - * AERE D-146: the chain the registries this fixture writes are BOUND to. It is the same value - * {@link #armAnchor} states in {@code aere.pq.chainId}: a registry bound to one chain and an - * anchor armed on another is a configuration this fixture must never accidentally describe. - */ - private static final long CHAIN_ID = 2_800L; - - @TempDir private Path tmp; - - @BeforeEach - public void setUp() throws Exception { - resetFalconSingleton(); - } - - @AfterEach - public void tearDown() throws Exception { - for (final String p : - new String[] { - "aere.falcon.genesis", - "aere.falcon.key", - "aere.falcon.attachBlock", - "aere.falcon.validatorCount", - "aere.falcon.testnetAllowSmallFleet", - PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, - PqAnchorConfig.PROPERTY_MIN_SEALS, - PqAnchorConfig.PROPERTY_CHAIN_ID - }) { - System.clearProperty(p); - } - // DATED 2026-08-20, the SECOND time this exact leak was paid for. armAnchor() plus - // FalconSealSupport.instance() builds the anchor config through PqAnchorProducer.config(), - // whose once-per-JVM cache outlives every property cleared above. Measured today on the - // production tree: the armed config this class caches turned all five PqFleetRestartArmingTest - // fixtures into AERE-PQC-REG-ARM-02 refusals, green alone, red in the suite, identical sources. - // The twin (PqForkThresholdReachabilityTest) has carried this line since 2026-08-11 with the - // same story; this class was forked before that fix and never received it. - PqAnchorProducer.useConfigForTesting(null); - resetFalconSingleton(); - } - - // ------------------------------------------------------------------------------------------- - // 1. THE FINDING. A threshold the fleet is not guaranteed to meet must not start. - // ------------------------------------------------------------------------------------------- - - @Test - public void armingAThresholdTheFleetCannotGuaranteeMustRefuseToStart() throws Exception { - // The exact state the standing plan walks through: the set has grown to nine, the anchored - // manifest still names the original seven, and the threshold is the one the schedule arms. - writeAnchoredRegistry(7); - System.setProperty("aere.falcon.validatorCount", "9"); - armAnchor(K); - - assertThatThrownBy(FalconSealSupport::instance) - .describedAs( - "N=9 with 7 keyed guarantees only %d Falcon seal(s) among a block's committers, and the " - + "armed threshold is K=%d. A node must refuse to start rather than arm a threshold " - + "no proposer is guaranteed to be able to meet.", - FalconSealSupport.worstCaseKeyedSigners(9, 7), K) - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("AERE-PQC-CFG-UNSAFE-08") - // The message has to carry BOTH numbers. "Unsafe" without them sends an operator to read - // code; the two numbers are the whole diagnosis and the whole remedy. - .hasMessageContaining("K=" + K) - .hasMessageContaining("guaranteed"); - } - - // ------------------------------------------------------------------------------------------- - // 2. NEGATIVE CONTROL. A guard that refuses everything is not a guard. - // ------------------------------------------------------------------------------------------- - - @Test - public void aReachableThresholdMustStillStart() throws Exception { - // N=7 fully keyed: quorum 5, guaranteed 5, K=5. Margin is exactly zero, which is a different - // statement from "unreachable", and the guard must not confuse the two. This is also the - // configuration the fleet runs today, so a guard that refused it would be a self-inflicted halt. - writeAnchoredRegistry(7); - System.setProperty("aere.falcon.validatorCount", "7"); - armAnchor(K); - - assertThatCode(FalconSealSupport::instance) - .describedAs("N=7 fully keyed guarantees exactly K=%d; zero margin is not unreachable", K) - .doesNotThrowAnyException(); - assertThat(FalconSealSupport.instance().registrySize()).isEqualTo(7); - } - - @Test - public void growingTheManifestWithTheSetIsWhatMakesNineSafe() throws Exception { - // The remedy the refusal names, measured rather than asserted: re-anchor the manifest for the - // whole set and the same N=9, same K=5 starts. - writeAnchoredRegistry(9); - System.setProperty("aere.falcon.validatorCount", "9"); - armAnchor(K); - - assertThatCode(FalconSealSupport::instance).doesNotThrowAnyException(); - assertThat(FalconSealSupport.worstCaseKeyedSigners(9, 9)) - .describedAs("nine keyed of nine guarantees the full ECDSA quorum") - .isEqualTo(6); - } - - // ------------------------------------------------------------------------------------------- - // 3. INERT WHERE IT MUST BE INERT. Chain 2800 as it stands today. - // ------------------------------------------------------------------------------------------- - - @Test - public void withNoAnchorConfiguredTheGuardIsInert() throws Exception { - // aere.pq.anchorBlock is UNSET on the live chain, so K does not exist and there is nothing to - // compare. A guard that could stop a node in that state would be a new way to lose the fleet, - // which is a strictly worse defect than the one it repairs. - writeAnchoredRegistry(7); - System.setProperty("aere.falcon.validatorCount", "9"); - - assertThatCode(FalconSealSupport::instance) - .describedAs("no anchor configured: no threshold, no comparison, no refusal") - .doesNotThrowAnyException(); - } - - @Test - public void aScheduleThatNeverRaisesTheThresholdAboveZeroIsInert() throws Exception { - writeAnchoredRegistry(7); - System.setProperty("aere.falcon.validatorCount", "9"); - armAnchor(0); - - assertThatCode(FalconSealSupport::instance) - .describedAs("K=0 everywhere is the warm-up regime; nothing can fail to be met") - .doesNotThrowAnyException(); - } - - // ------------------------------------------------------------------------------------------- - // 4. THE CASE WITH NO KEYS AT ALL, which is the same arithmetic at its floor. - // ------------------------------------------------------------------------------------------- - - @Test - public void aPositiveThresholdWithNoAnchoredKeysMustRefuseToStart() throws Exception { - // No manifest anywhere and K=5: guaranteed is 0, so every block at or above H would be rejected - // for want of a certificate nobody can produce. Distinct from the A8 refusal, which only fires - // when aere.falcon.forkBlock is set; the anchor path has its own arming height. - System.setProperty("aere.falcon.validatorCount", "7"); - armAnchor(K); - - assertThatThrownBy(FalconSealSupport::instance) - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("AERE-PQC-CFG-UNSAFE-08"); - } - - // ------------------------------------------------------------------------------------------- - // 5. THE WAIVER IS EXPLICIT, NAMED, AND ONLY FOR ISOLATED NETWORKS. - // ------------------------------------------------------------------------------------------- - - @Test - public void anIsolatedTestNetworkCanWaiveTheGuardExplicitly() throws Exception { - writeAnchoredRegistry(7); - System.setProperty("aere.falcon.validatorCount", "9"); - System.setProperty("aere.falcon.testnetAllowSmallFleet", "true"); - armAnchor(K); - - assertThatCode(FalconSealSupport::instance) - .describedAs( - "the same switch that waives the N>=9 rule waives this one, because both say the same " - + "thing: this fleet has no Falcon fault margin and must not be a mainnet") - .doesNotThrowAnyException(); - } - - // ------------------------------------------------------------------------------------------- - // 6. THE ARITHMETIC ITSELF, at the boundary, as a pure function. - // ------------------------------------------------------------------------------------------- - - @Test - public void theDeficitIsTheDistanceBetweenTheThresholdAndTheGuarantee() { - assertThat(FalconSealSupport.thresholdDeficit(7, 7, 5)) - .describedAs("N=7 fully keyed meets K=5 exactly") - .isZero(); - assertThat(FalconSealSupport.thresholdDeficit(8, 7, 5)) - .describedAs("one unkeyed validator added: still met") - .isZero(); - assertThat(FalconSealSupport.thresholdDeficit(9, 7, 5)) - .describedAs("two added without re-anchoring: short by one, which is the halt") - .isEqualTo(1); - assertThat(FalconSealSupport.thresholdDeficit(9, 9, 5)).isZero(); - assertThat(FalconSealSupport.thresholdDeficit(7, 0, 1)) - .describedAs("no keys at all: a positive threshold is short by all of it") - .isEqualTo(1); - assertThat(FalconSealSupport.thresholdDeficit(7, 7, 0)) - .describedAs("K=0 can never be in deficit") - .isZero(); - } - - // ------------------------------------------------------------------------------------------- - // Helpers. - // ------------------------------------------------------------------------------------------- - - /** Arm the V2 anchor from system configuration with a staged threshold that reaches {@code k}. */ - private static void armAnchor(final int k) { - System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(H)); - // AERE CONFIGURATIE-STRICTA (2026-08-06): an activation height without an explicit - // chain id is now a startup refusal, because a silently defaulted 0 in the D and M - // pre-images is the Holesky shape. The fixture states what the fleet states. - System.setProperty(PqAnchorConfig.PROPERTY_CHAIN_ID, Long.toString(CHAIN_ID)); - System.setProperty( - PqAnchorConfig.PROPERTY_MIN_SEALS, H + ":0," + K_AT + ":" + k); - } - - /** - * Write a genesis-anchored, address-bound Falcon manifest for {@code count} validators and point - * this node at index 0's key, exactly as {@code D078ValidatorSetChangeTest} does. The anchored hash - * is accumulated in lockstep with the manifest text, so the fixture is anchored the way a real - * genesis is rather than by a flag. - */ - private void writeAnchoredRegistry(final int count) throws Exception { - // AERE D-146 (2026-08-06): v2, proof-bound, bound at H, the height armAnchor() arms from. The - // rows come from PqV2Fixture because a v2 claim must be signed by the validator whose address - // is on the row, and the 0xA00+i addresses this used to spell have no key behind them. - final KeccakDigest kd = new KeccakDigest(256); - final StringBuilder manifest = new StringBuilder(); - manifest - .append("{\"config\":{\"aereFalconRegistry\":{") - .append(PqV2Fixture.manifestHeader(count, CHAIN_ID, H)); - for (int i = 0; i < count; i++) { - final FalconPrivateKeyParameters priv = PqV2Fixture.privateKey(i); - final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i); - kd.update(anchoredRow, 0, anchoredRow.length); - manifest.append(',').append(PqV2Fixture.manifestEntry(i, count, CHAIN_ID, H)); - if (i == 0) { - final Path key0 = tmp.resolve("falcon-key-0.properties"); - Files.writeString( - key0, - "index=0\n" - + "f=" - + Bytes.wrap(priv.getSpolyf()).toHexString() - + "\n" - + "g=" - + Bytes.wrap(priv.getG()).toHexString() - + "\n" - + "F=" - + Bytes.wrap(priv.getSpolyF()).toHexString() - + "\n" - + "pk=" - + Bytes.wrap(PqV2Fixture.publicKey(i)).toHexString() - + "\n"); - System.setProperty("aere.falcon.key", key0.toAbsolutePath().toString()); - } - } - manifest.append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x") - .append("0".repeat(64)) - .append("\":\"0x"); - final byte[] anchoredHash = new byte[32]; - kd.doFinal(anchoredHash, 0); - manifest.append(Bytes.wrap(anchoredHash).toUnprefixedHexString()).append("\"}}}}"); - - final Path genesis = tmp.resolve("genesis-registry.json"); - Files.writeString(genesis, manifest.toString()); - System.setProperty("aere.falcon.genesis", genesis.toAbsolutePath().toString()); - } - - private static void resetFalconSingleton() throws Exception { - final Field f = FalconSealSupport.class.getDeclaredField("instance"); - f.setAccessible(true); - f.set(null, null); - } -} - diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ValidatorSetChangeTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ValidatorSetChangeTest.java deleted file mode 100644 index 836c179..0000000 --- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D078ValidatorSetChangeTest.java +++ /dev/null @@ -1,428 +0,0 @@ -/* - * 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.consensus.common.bft; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; - -import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer; -import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.ProtocolContext; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; - -import java.lang.reflect.Field; -import java.nio.file.Files; -import java.nio.file.Path; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalInt; - -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; -import org.bouncycastle.crypto.digests.KeccakDigest; -import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters; -import org.bouncycastle.pqc.crypto.falcon.FalconSigner; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import org.mockito.quality.Strictness; - -/** - * D-078. THE MEASUREMENT THAT DID NOT EXIST. - * - *
The registry entry reads: "if PQC were armed, an ordinary add-validator vote would stop the - * chain: the Falcon blocking quorum follows the dynamic set and cannot be reached inside the vote - * window", and it carried {@code verifica: NICIUNA} because "the direct measurement would require - * ARMING PQC on a chain, which is exactly the thing that stops the chain". - * - *
That is true of a whole chain. It is NOT true of the decision that stops it. Every step from - * "the validator set changed" to "no block can be proposed" is taken by three objects in this - * module, each of which is a pure function of its inputs: {@link FalconSealSupport#attachmentArmed} - * decides whether this node emits a Falcon seal at all, {@link PqSealCache} holds what was heard, - * and {@link PqAnchorProducer#apply} decides whether this node may propose. This class drives those - * three with a REAL address-bound genesis-anchored registry and REAL Falcon-512 keys, and asks the - * question the registry says cannot be asked. - * - *
WHAT EACH TEST MEASURES, and why each of them can fail: - * - *
NOT MEASURED here, deliberately, and named so it is not mistaken for covered: how many rounds a - * live fleet takes to stop once every proposer refuses, and what a syncing node does meanwhile. - * Those need a network, and the network run is separate evidence. - */ -public class D078ValidatorSetChangeTest { - - /** Anchor activation height H used throughout. */ - private static final long H = 1_000L; - - /** Seal-attachment height, comfortably below H. */ - private static final long ATTACH = 900L; - - /** Height from which the staged threshold K is 5, i.e. the armed regime. */ - private static final long K_AT = H + 10L; - - private static final int K = 5; - - private static final int N = 7; - - private static final long CHAIN_ID = 220_878L; - - @TempDir private Path tmp; - - private final List
keyedValidators = new ArrayList<>(); - private final ListThe three rows below are the ones that decide the project's own arming order, so they are - * measured here rather than reasoned about in a document: - * - *
- * N=7, keyed 7, quorum 5 -> 5 guaranteed K=5 is met, with EXACTLY zero margin - * N=8, keyed 7, quorum 6 -> 5 guaranteed K=5 is still met, still zero margin - * N=9, keyed 7, quorum 6 -> 4 guaranteed K=5 is NOT guaranteed any more - *- * - *
Read against the standing rule "grow to N=9 BEFORE arming", that third row is the warning: - * growing to nine while the anchored manifest still names seven is exactly the state in which a - * proposer can legitimately fail to assemble a certificate. The manifest has to grow with the set. - */ - @Test - public void theCostOfAnUnkeyedValidatorIsANumberAndTheNumberIsThis() { - assertThat(FalconSealSupport.worstCaseKeyedSigners(7, 7)) - .describedAs("N=7 fully keyed: K=5 is met with zero margin") - .isEqualTo(5); - assertThat(FalconSealSupport.worstCaseKeyedSigners(8, 7)) - .describedAs("one validator added without re-anchoring: K=5 still met, still zero margin") - .isEqualTo(5); - assertThat(FalconSealSupport.worstCaseKeyedSigners(9, 7)) - .describedAs( - "two added without re-anchoring: below K=5, so a proposer can legitimately fail. This " - + "is the row that constrains growing to N=9 before arming.") - .isEqualTo(4); - assertThat(FalconSealSupport.worstCaseKeyedSigners(7, 0)).isZero(); - assertThat(FalconSealSupport.worstCaseKeyedSigners(0, 0)).isZero(); - } - - // ----------------------------------------------------------------------------------------- - // 5. NEGATIVE CONTROL for this whole file: the gate must still refuse what it must refuse. - // ----------------------------------------------------------------------------------------- - - /** - * Every other test here asserts that the gate says YES. Replace {@code attachmentArmed} with - * {@code return true} and all of them still pass, which would make this file a proof that cannot - * fail. These four assertions are what makes that substitution impossible: each names a condition - * the D-078 repair deliberately did NOT touch. - * - * @throws Exception if the fixture cannot be rebuilt - */ - @Test - public void theGateStillRefusesEverythingItMustStillRefuse() throws Exception { - // (1) below the configured attachment height. - assertThat(FalconSealSupport.instance().attachmentArmed(ATTACH - 1L)) - .describedAs("below the attachment height nothing may be attached") - .isFalse(); - - // (2) no attachment height configured at all, which is the default and the state of chain 2800. - System.clearProperty("aere.falcon.attachBlock"); - resetFalconSingleton(); - assertThat(FalconSealSupport.instance().attachmentArmed(H + 5L)) - .describedAs("with aere.falcon.attachBlock unset a node holding a key attaches nothing") - .isFalse(); - - // (3) attachment height reached, but no anchored registry to be checked against. - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.clearProperty("aere.falcon.genesis"); - resetFalconSingleton(); - assertThat(FalconSealSupport.instance().attachmentArmed(H + 5L)) - .describedAs("a seal is never emitted against a registry that cannot be checked") - .isFalse(); - - // (4) anchored, address-bound registry, but it does not bind THIS node's index. The seal would - // be unattributable, so the seals rule would refuse the whole header carrying it. - System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString()); - final Path strayKey = tmp.resolve("falcon-key-stray.properties"); - Files.writeString(strayKey, Files.readString(key0Path).replace("index=0", "index=42")); - System.setProperty("aere.falcon.key", strayKey.toAbsolutePath().toString()); - resetFalconSingleton(); - final FalconSealSupport stray = FalconSealSupport.instance(); - assertThat(stray.genesisAnchored()) - .describedAs("the registry must still load, or (4) would pass for the wrong reason") - .isTrue(); - assertThat(stray.attachmentArmed(H + 5L)) - .describedAs("an index the anchored registry does not bind must not attach") - .isFalse(); - assertThat(stray.sign(H + 5L, message(H + 4L))).isEmpty(); - } - - // ----------------------------------------------------------------------------------------- - // Helpers. - // ----------------------------------------------------------------------------------------- - - private static Bytes32 message(final long blockNumber) { - return PqAnchor.commitMessage(CHAIN_ID, blockNumber, Bytes32.leftPad(Bytes.of(1))); - } - - private static byte[] falconSign(final FalconPrivateKeyParameters key, final Bytes32 m) { - final FalconSigner signer = new FalconSigner(); - signer.init(true, key); - return signer.generateSignature(m.toArray()); - } - - private static ProtocolContext contextWith(final Collection
validators) { - final ValidatorProvider validatorProvider = - mock(ValidatorProvider.class, withSettings().strictness(Strictness.LENIENT)); - when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators); - when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(validators); - final BftContext bftContext = - mock(BftContext.class, withSettings().strictness(Strictness.LENIENT)); - when(bftContext.getValidatorProvider()).thenReturn(validatorProvider); - when(bftContext.as(any())).thenReturn(bftContext); - return new ProtocolContext.Builder().withConsensusContext(bftContext).build(); - } - - private static void resetFalconSingleton() throws Exception { - final Field f = FalconSealSupport.class.getDeclaredField("instance"); - f.setAccessible(true); - f.set(null, null); - } -} diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D079ForkArmingTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D079ForkArmingTest.java deleted file mode 100644 index c1067e6..0000000 --- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D079ForkArmingTest.java +++ /dev/null @@ -1,368 +0,0 @@ -/* - * 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.consensus.common.bft; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - - -import java.lang.reflect.Field; -import java.nio.file.Files; -import java.nio.file.Path; - -import org.apache.tuweni.bytes.Bytes; -import org.bouncycastle.crypto.digests.KeccakDigest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -/** - * D-079. THE MEASUREMENT THAT DID NOT EXIST. - * - *The registry entry reads: "a malformed forkBlock falls OPEN, with only a log line, and arming - * it at or before the anchor observation height passes undetected", and it carried {@code verifica: - * NICIUNA} since 18 July. This file is the command that can fail. - * - *
Both halves of the finding are about the SAME shape of defect, the one the Holesky Pectra - * incident of February 2025 made expensive for everybody: a fork-activation parameter that is wrong - * or absent does not stop the node, it changes what the node silently believes. Half one is the - * value itself. Half two is the ORDER between that value and the height at which the registry the - * value depends on becomes active. - * - *
WHAT EACH TEST MEASURES, and how each can fail: - * - *
NOT MEASURED here, deliberately, and named so it is not mistaken for covered: whether a real - * Besu node process exits with a non-zero status when this exception is thrown. This class measures - * the decision, not the process. The exception is thrown from the constructor, on the same path as - * the guards that already abort, and nothing in this tree catches {@code - * FalconSealSupport.ActivationConfigException}. - */ -public class D079ForkArmingTest { - - /** Fleet size; nine, because the blocking guard refuses to arm below nine. */ - private static final int N = 9; - - /** Height at which the on-chain late-anchor registry contract is expected to be observed. */ - private static final long OBSERVE = 5_000L; - - /** Seal-attachment height: at or after OBSERVE, so a seal can actually be emitted. */ - private static final long ATTACH = 6_000L; - - /** Blocking height: at least minAttachLead (256) after ATTACH. */ - private static final long FORK = 7_000L; - - private static final String ANCHOR_ADDRESS = "0x0000000000000000000000000000000000000fa1"; - - /** - * AERE D-146: the chain this fixture's registries are BOUND to. Every proof commits to it, so it - * has to be stated rather than defaulted. - */ - private static final long CHAIN_ID = 2_800L; - - @TempDir private Path tmp; - - private Path manifestPath; - private Path genesisPath; - - @BeforeEach - public void setUp() throws Exception { - // AERE D-146 (2026-08-06): both registries below are v2 and PROOF-BOUND, bound at FORK, the - // height this fixture arms from. They used to carry addresses spelled 0xB00+i, which no - // secp256k1 key can sign for, so this whole fixture became unstartable the moment - // AERE-PQC-REG-ARM-02 was wired into the constructor. - - // LATE-ANCHOR manifest: the registry is PENDING until the anchor contract is observed on chain. - final StringBuilder late = new StringBuilder("{"); - late.append(PqV2Fixture.manifestHeader(N, CHAIN_ID, FORK)); - for (int i = 0; i < N; i++) { - late.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, FORK)); - } - late.append("}"); - manifestPath = tmp.resolve("falcon-late-manifest.json"); - Files.writeString(manifestPath, late.toString()); - - // GENESIS-ANCHORED manifest: the registry is ACTIVE from block 0, so no observation height can - // exist and none may be demanded. Built exactly the way a real genesis is, hash included. - final KeccakDigest kd = new KeccakDigest(256); - final StringBuilder gen = new StringBuilder("{\"config\":{\"aereFalconRegistry\":{"); - gen.append(PqV2Fixture.manifestHeader(N, CHAIN_ID, FORK)); - for (int i = 0; i < N; i++) { - final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i); - kd.update(anchoredRow, 0, anchoredRow.length); - gen.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, FORK)); - } - final byte[] anchoredHash = new byte[32]; - kd.doFinal(anchoredHash, 0); - gen.append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x") - .append("0".repeat(64)) - .append("\":\"0x") - .append(Bytes.wrap(anchoredHash).toUnprefixedHexString()) - .append("\"}}}}"); - genesisPath = tmp.resolve("genesis-registry.json"); - Files.writeString(genesisPath, gen.toString()); - - System.setProperty("aere.falcon.validatorCount", Integer.toString(N)); - resetFalconSingleton(); - } - - @AfterEach - public void tearDown() throws Exception { - for (final String p : - new String[] { - "aere.falcon.manifest", - "aere.falcon.genesis", - "aere.falcon.anchor.address", - "aere.falcon.anchor.block", - "aere.falcon.attachBlock", - "aere.falcon.forkBlock", - "aere.falcon.validatorCount" - }) { - System.clearProperty(p); - } - resetFalconSingleton(); - } - - // ------------------------------------------------------------------------------------------- - // 1. The fixture's own control. - // ------------------------------------------------------------------------------------------- - - @Test - public void controlAWellFormedLateAnchorConfigurationStarts() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE)); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", Long.toString(FORK)); - - final FalconSealSupport pqc = FalconSealSupport.instance(); - assertThat(pqc.lateAnchorPending()) - .describedAs( - "the late-anchor manifest must load and stay PENDING, or every refusal below is a " - + "refusal about a registry that was never there") - .isTrue(); - assertThat(pqc.forkBlock()).isEqualTo(FORK); - assertThat(pqc.attachBlock()).isEqualTo(ATTACH); - assertThat(pqc.forkBlock()) - .describedAs( - "the ordering the guard exists to enforce, stated as a property: blocking arms strictly " - + "AFTER the height at which the registry it depends on can become active") - .isGreaterThan(OBSERVE); - } - - // ------------------------------------------------------------------------------------------- - // 2-3. Half one at config time. - // ------------------------------------------------------------------------------------------- - - @Test - public void aMalformedForkBlockRefusesToStart() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE)); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - // The exact typo shape a human makes when copying a height out of a document. - System.setProperty("aere.falcon.forkBlock", "9_189_161"); - - assertThatThrownBy(FalconSealSupport::instance) - .describedAs( - "a malformed blocking height must ABORT, never degrade to never-blocking with a log line") - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("MALFORMED"); - } - - @Test - public void aNegativeForkBlockRefusesToStart() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE)); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", "-1"); - - assertThatThrownBy(FalconSealSupport::instance) - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("negative"); - } - - // ------------------------------------------------------------------------------------------- - // 4. Half one where it actually survived: the value was validated but never OWNED. - // ------------------------------------------------------------------------------------------- - - @Test - public void theForkBlockIsResolvedOnceAndCannotBeReopenedAfterStartup() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE)); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", Long.toString(FORK)); - - final FalconSealSupport pqc = FalconSealSupport.instance(); - assertThat(pqc.forkBlock()).isEqualTo(FORK); - - // The startup guard has already run and passed. Nothing will run it again. If the accessor - // re-reads the property, then the ONE decision the whole PQC layer is gated on is a value that - // can still turn into "never blocking" at any moment, for any reason that leaves the property - // unparseable, and the only trace is one WARN line per call. - System.setProperty("aere.falcon.forkBlock", "not-a-number"); - assertThat(pqc.forkBlock()) - .describedAs( - "the blocking height must be resolved ONCE, at the boundary, and owned thereafter. A " - + "value that is validated at startup and re-parsed on every use is not validated.") - .isEqualTo(FORK); - } - - // ------------------------------------------------------------------------------------------- - // 5-7. Half two: the ORDER between the blocking height and the anchor observation height. - // ------------------------------------------------------------------------------------------- - - @Test - public void blockingOverAPendingAnchorWithNoDeclaredObservationHeightRefuses() { - lateAnchor(); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", Long.toString(FORK)); - // aere.falcon.anchor.block deliberately NOT set. - - assertThatThrownBy(FalconSealSupport::instance) - .describedAs( - "with the registry still PENDING and no stated activation height, nothing in this " - + "process can compare the blocking height against the height at which the registry " - + "becomes usable, so the ordering error the finding names cannot be detected at all") - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("AERE-PQC-CFG-UNSAFE-06"); - } - - @Test - public void anAttachHeightBeforeTheObservationHeightRefuses() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", Long.toString(ATTACH + 1L)); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", Long.toString(FORK)); - - assertThatThrownBy(FalconSealSupport::instance) - .describedAs( - "attachment before the registry can be active emits nothing, so the log-only soak " - + "window measures nothing and the blocking height arrives over a registry no node " - + "has ever produced a seal against") - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("AERE-PQC-CFG-UNSAFE-07"); - } - - @Test - public void aForkHeightAtTheObservationHeightRefuses() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", Long.toString(FORK)); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", Long.toString(FORK)); - - assertThatThrownBy(FalconSealSupport::instance) - .describedAs("the literal stimulus in the finding: armed AT the anchor observation height") - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("AERE-PQC-CFG-UNSAFE-07"); - } - - // ------------------------------------------------------------------------------------------- - // 8-9. The new value must fail closed like every other one. - // ------------------------------------------------------------------------------------------- - - @Test - public void aMalformedObservationHeightRefuses() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", "1e3"); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", Long.toString(FORK)); - - assertThatThrownBy(FalconSealSupport::instance) - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("AERE-PQC-CFG-SYNTAX-09"); - } - - @Test - public void aNegativeObservationHeightRefuses() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", "-5"); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", Long.toString(FORK)); - - assertThatThrownBy(FalconSealSupport::instance) - .isInstanceOf(FalconSealSupport.ActivationConfigException.class) - .hasMessageContaining("AERE-PQC-CFG-SYNTAX-10"); - } - - // ------------------------------------------------------------------------------------------- - // 10-11. Scope controls. A guard that refuses everything is not a guard. - // ------------------------------------------------------------------------------------------- - - @Test - public void aGenesisAnchoredRegistryNeedsNoObservationHeight() { - System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString()); - System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH)); - System.setProperty("aere.falcon.forkBlock", Long.toString(FORK)); - // aere.falcon.anchor.block deliberately NOT set: a genesis-anchored registry is active from - // block 0, so there IS no observation height and demanding one would break the whole - // genesis-anchored deployment path. - - final FalconSealSupport pqc = FalconSealSupport.instance(); - assertThat(pqc.genesisAnchored()).isTrue(); - assertThat(pqc.addressBound()).isTrue(); - assertThat(pqc.forkBlock()).isEqualTo(FORK); - } - - @Test - public void anObservationHeightWithoutBlockingIsHarmless() { - lateAnchor(); - System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE)); - // No forkBlock, no attachBlock: the log-only baseline every node on chain 2800 runs today. - - final FalconSealSupport pqc = FalconSealSupport.instance(); - assertThat(pqc.forkBlock()).isEqualTo(Long.MAX_VALUE); - assertThat(pqc.attachBlock()).isEqualTo(Long.MAX_VALUE); - assertThat(pqc.lateAnchorPending()).isTrue(); - } - - // ------------------------------------------------------------------------------------------- - // Helpers. - // ------------------------------------------------------------------------------------------- - - private void lateAnchor() { - System.setProperty("aere.falcon.manifest", manifestPath.toAbsolutePath().toString()); - System.setProperty("aere.falcon.anchor.address", ANCHOR_ADDRESS); - } - - private static void resetFalconSingleton() throws Exception { - final Field f = FalconSealSupport.class.getDeclaredField("instance"); - f.setAccessible(true); - f.set(null, null); - } -} diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D081RegistryRotationTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D081RegistryRotationTest.java deleted file mode 100644 index 12c091e..0000000 --- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D081RegistryRotationTest.java +++ /dev/null @@ -1,484 +0,0 @@ -/* - * 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.consensus.common.bft; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -/** - * D-081: the Falcon signer registry has no usable rotation and no usable revocation. - * - *
WHAT IS MEASURED HERE, and why it is measured against the real classes rather than described. - * {@code config.pqRegistryHash} is a SCHEDULE of {block, hash} entries, and the A8 dossier states - * that "a later entry expresses a key rotation". This file asks whether that sentence survives - * contact with the code that enforces it. - * - *
The enforcement side is {@link PqRegistryHash#matchesAt} and, on the block path, {@code - * FalconSealSupport.registryBindingSatisfiedAt(height)}, which calls it. Both take exactly ONE - * loaded registry, and the node loads exactly one file ({@code aere.falcon.registry}). The entry the - * schedule makes active at a height decides which hash is required THERE. So after one rotation at - * H2 there are two intervals with two different required hashes, and one file can satisfy at most - * one of them. - * - *
The consequence is not cosmetic and it is not confined to the rotation moment. {@code - * PqRegistryBindingRule} is a DETACHED rule, so it runs on the header-download path, and {@code - * PqAnchorSyncModeGuard} refuses to start an armed node in anything but FULL sync. A node acquiring - * history therefore validates every height, including the interval before the rotation. Holding the - * post-rotation registry it is refused there; holding the pre-rotation registry it is refused at the - * head. There is no third choice. ONE rotation makes the chain permanently unjoinable. - * - *
This is the lesson Cosmos ADR-016 writes down explicitly: a rotation scheme has to keep the - * MAPPING FROM HEIGHT TO KEY SET, not only the current key set, or blocks signed under the old set - * stop being verifiable. Cosmos may bound that history by the unbonding period. We may not: chain - * 2800 has no unbonding period and a node syncing from genesis must verify every block that was ever - * produced, so every entry ever scheduled has to stay loadable forever. - * - *
{@code rotationDoesNotBrickHistory} and {@code revocationDoesNotBrickHistory} are the - * measurement. They FAIL while the defect is present and pass only when a node can be configured to - * satisfy the binding at EVERY scheduled height at once. The other tests are controls: they assert - * that the schedule really does express rotation and really does refuse a malformed one, so a - * failure of the two measurements cannot be blamed on the fixture. - */ -public class D081RegistryRotationTest { - - private static final long CHAIN_ID = 2800L; - - /** First binding height: the height the post-quantum registry is first enforced from. */ - private static final long H1 = 12_000_000L; - - /** Rotation height: from here the chain requires the SECOND registry. */ - private static final long H2 = 12_100_000L; - - /** Falcon-512 public key length as this registry format stores it (bare h polynomial). */ - private static final int PK_LENGTH = 896; - - /** The seven validators of chain 2800. */ - private static final int N = 7; - - /** - * One node configuration, expressed as the only question the consensus path ever asks it: does - * the registry material this node holds satisfy the binding the chain requires at this height? - * - *
It is an interface and not a Registry so that the measurement can be stated once and asked of
- * every configuration a node can actually be put into. Today there is exactly one shape of answer,
- * {@link #single}, because a node loads one file. A repair that lets a node hold the whole
- * scheduled history adds a second shape here and the assertion below stops failing. Nothing in the
- * assertion has to change, which is the point: the property is fixed, the capability is what moves.
- */
- private interface NodeConfiguration {
- boolean satisfiesAt(long height);
-
- String describe();
- }
-
- private static NodeConfiguration single(
- final String name, final PqRegistryHash.Schedule schedule, final PqRegistryHash.Registry r) {
- return new NodeConfiguration() {
- @Override
- public boolean satisfiesAt(final long height) {
- return PqRegistryHash.matchesAt(schedule, r, height, CHAIN_ID);
- }
-
- @Override
- public String describe() {
- return "node holding only registry " + name;
- }
- };
- }
-
- // ---------------------------------------------------------------------------------------
- // Fixture. Two registries that differ in exactly one row, which is what both a rotation and a
- // revocation look like on the wire: index 3 stops being the key it was.
- // ---------------------------------------------------------------------------------------
-
- private static byte[] deterministicKey(final int index, final int generation) {
- final byte[] pk = new byte[PK_LENGTH];
- for (int i = 0; i < pk.length; i++) {
- pk[i] = (byte) ((i * 31) + (index * 7) + (generation * 101));
- }
- return pk;
- }
-
- private static byte[] address(final int index) {
- final byte[] a = new byte[20];
- for (int i = 0; i < a.length; i++) {
- a[i] = (byte) ((index * 17) + i);
- }
- return a;
- }
-
- private static String hex(final byte[] b) {
- final StringBuilder sb = new StringBuilder(b.length * 2);
- for (final byte x : b) {
- sb.append(String.format("%02x", x));
- }
- return sb.toString();
- }
-
- /**
- * A seven-row address-bound registry. {@code rotatedIndex} is the row whose key belongs to
- * generation 2; every other row is generation 1. Passing -1 gives the untouched registry.
- */
- private static Path writeRegistry(final Path dir, final String name, final int rotatedIndex)
- throws IOException {
- final StringBuilder sb = new StringBuilder();
- sb.append("count=").append(N).append('\n');
- for (int i = 0; i < N; i++) {
- sb.append(i).append('=').append(hex(deterministicKey(i, i == rotatedIndex ? 2 : 1))).append('\n');
- sb.append(i).append(".addr=").append(hex(address(i))).append('\n');
- }
- final Path p = dir.resolve(name);
- Files.write(p, sb.toString().getBytes(StandardCharsets.UTF_8));
- return p;
- }
-
- private static PqRegistryHash.Schedule scheduleOf(final String hashAtH1, final String hashAtH2) {
- final String json =
- "[{\"block\":"
- + H1
- + ",\"hash\":\"0x"
- + hashAtH1
- + "\"},{\"block\":"
- + H2
- + ",\"hash\":\"0x"
- + hashAtH2
- + "\"}]";
- final JsonNode node;
- try {
- node = new ObjectMapper().readTree(json);
- } catch (final IOException e) {
- throw new IllegalStateException(e);
- }
- return PqRegistryHash.parseSchedule(node, "D-081 fixture");
- }
-
- /** Every height at which the binding is enforced and could differ across the rotation. */
- private static List MEASURED FIRST, ON A NETWORK, NOT ASSUMED. The full activation rehearsal on a seven-node test
- * network (repetitie-activare-2026-08-05) found that with the anchor armed at K>0 a SIMULTANEOUS
- * restart of every validator stops the chain for good. The node said it verbatim: "refusing to
- * propose ... holds 0 valid eligible Falcon seal(s) ... threshold is 3", over "Attachment stays OFF
- * (fail-safe)".
- *
- * THE CIRCLE. {@code activateLateAnchor()} used to be reachable from exactly one place, {@code
- * FalconSealValidationRule.tryActivateLateAnchor}, which runs only while a block is being IMPORTED.
- * After a fleet restart no block is imported, because nobody proposes. So {@code lateActivated}
- * stays false, {@link FalconSealSupport#attachmentArmed(long)} answers false, no seal is attached,
- * no certificate reaches K, and nobody can propose. Seals come from Commits, Commits come from
- * proposals, proposals need seals. With K=0 the chain heals itself. With K>0 it never does.
- *
- * WHAT THIS CLASS MEASURES, and it is the state machine the repair moves, not a paraphrase of
- * it. The repair (QbftBesuControllerBuilder, marker "AERE BLOCAJ-REPORNIRE") adds a SECOND caller of
- * the SAME method at startup, reading the SAME contract slot 0 out of the chain-head world state.
- * So the question that decides whether the repair can work is exactly: does calling {@code
- * activateLateAnchor} with the on-chain hash, with no block imported and no other stimulus, turn
- * {@code attachmentArmed()} from false to true. Below, it does.
- *
- * NOT MEASURED here, and named so it is not read as covered: that a real Besu process reads slot
- * 0 out of a real chain-head world state (that is world-state plumbing in the app module, and the
- * rehearsal network is the instrument for it), and that seven live nodes recover from a real
- * simultaneous restart with this binary. This class measures the decision the deadlock hinges on.
- */
-public class D140FleetRestartArmingTest {
-
- /**
- * Fleet size for THIS fixture. Not a statement about any live network: the 2026-08-05 decision
- * to stay at seven was reversed, and the set has been nine since 2026-08-12. Seven is kept here
- * because it is the size at which the margin arithmetic this class exercises is tightest.
- */
- private static final int N = 7;
-
- /** Height at which the anchor contract is expected to be observable. */
- private static final long OBSERVE = 1_000L;
-
- /** Seal-attachment height, at or after OBSERVE. */
- private static final long ATTACH = 1_200L;
-
- /** A chain head well past the attachment height: this is what a restart comes back to. */
- private static final long HEAD = 5_000L;
-
- private static final String ANCHOR_ADDRESS = "0x0000000000000000000000000000000000000fa1";
-
- @TempDir private Path tmp;
-
- /** keccak256 over (addr20 || pk) for every index in order: what the anchor contract holds. */
- private String onChainHash;
-
- /** The same registry, spelled as a GENESIS-anchored manifest (the rehearsal's own shape). */
- private Path genesisPath;
-
- @BeforeEach
- public void setUp() throws Exception {
- // ADDED 2026-08-29 (finding D-293), and it was MISSING here while its twin had carried it since
- // 2026-08-20. That is the whole danger of keeping two copies of a test: a repair lands in one
- // and not the other, and nothing fails to say so. This class never arms the certificate anchor,
- // but FalconSealSupport's constructor consults it (anchorArmedFrom() -> PqAnchorProducer.config(),
- // a per-JVM cache): a neighbouring class that leaves an ARMED config cached turns every
- // proof-less fixture below into an AERE-PQC-REG-ARM-02 refusal. Measured on the production tree:
- // alone 5/5 green, inside the full suite the same 5 red, identical sources. Clearing the
- // properties does not clear the caches.
- for (final String p : System.getProperties().stringPropertyNames()) {
- if (p.startsWith("aere.pq.")) {
- System.clearProperty(p);
- }
- }
- org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.useConfigForTesting(
- null);
- resetFalconSingleton();
-
- final SecureRandom rnd = SecureRandomProvider.createSecureRandom();
- final KeccakDigest kd = new KeccakDigest(256);
- final StringBuilder manifest = new StringBuilder("{\"count\":").append(N);
- final StringBuilder genesis =
- new StringBuilder("{\"config\":{\"aereFalconRegistry\":{\"count\":").append(N);
- for (int i = 0; i < N; i++) {
- final FalconKeyPairGenerator gen = new FalconKeyPairGenerator();
- gen.init(new FalconKeyGenerationParameters(rnd, FalconParameters.falcon_512));
- final AsymmetricCipherKeyPair kp = gen.generateKeyPair();
- final FalconPublicKeyParameters pub = (FalconPublicKeyParameters) kp.getPublic();
- final FalconPrivateKeyParameters priv = (FalconPrivateKeyParameters) kp.getPrivate();
- final Address addr = Address.fromHexString(String.format("0x%040x", 0xA00 + i));
-
- // The pre-image is accumulated in lockstep with the manifest text, exactly the way a real
- // anchoring transaction is built, so the hash below is not copied out of the code under test.
- final byte[] addrBytes = addr.getBytes().toArray();
- kd.update(addrBytes, 0, addrBytes.length);
- kd.update(pub.getH(), 0, pub.getH().length);
-
- final String entry =
- ",\""
- + i
- + "\":{\"addr\":\""
- + addr.toHexString()
- + "\",\"pk\":\""
- + Bytes.wrap(pub.getH()).toHexString()
- + "\"}";
- manifest.append(entry);
- genesis.append(entry);
-
- if (i == 0) {
- // This node is validator 0 and HOLDS a signing key, otherwise attachment is off for a
- // reason that has nothing to do with the deadlock and the measurement would be vacuous.
- final Path key0 = tmp.resolve("falcon-key-0.properties");
- Files.writeString(
- key0,
- "index=0\n"
- + "f="
- + Bytes.wrap(priv.getSpolyf()).toHexString()
- + "\n"
- + "g="
- + Bytes.wrap(priv.getG()).toHexString()
- + "\n"
- + "F="
- + Bytes.wrap(priv.getSpolyF()).toHexString()
- + "\n"
- + "pk="
- + Bytes.wrap(pub.getH()).toHexString()
- + "\n");
- System.setProperty("aere.falcon.key", key0.toAbsolutePath().toString());
- }
- }
- manifest.append("}");
-
- final byte[] digest = new byte[32];
- kd.doFinal(digest, 0);
- onChainHash = Bytes.wrap(digest).toUnprefixedHexString();
-
- final Path manifestPath = tmp.resolve("falcon-late-manifest.json");
- Files.writeString(manifestPath, manifest.toString());
-
- // Same seven entries, anchored the way the rehearsal network anchored them: in genesis, with
- // the hash committed in the anchor contract's slot 0 through alloc storage.
- genesis
- .append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
- .append("0".repeat(64))
- .append("\":\"0x")
- .append(onChainHash)
- .append("\"}}}}");
- genesisPath = tmp.resolve("genesis-registry.json");
- Files.writeString(genesisPath, genesis.toString());
-
- System.setProperty("aere.falcon.manifest", manifestPath.toAbsolutePath().toString());
- System.setProperty("aere.falcon.anchor.address", ANCHOR_ADDRESS);
- System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE));
- System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
- System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
- }
-
- @AfterEach
- public void tearDown() throws Exception {
- for (final String p :
- new String[] {
- "aere.falcon.manifest",
- "aere.falcon.genesis",
- "aere.falcon.key",
- "aere.falcon.anchor.address",
- "aere.falcon.anchor.block",
- "aere.falcon.attachBlock",
- "aere.falcon.forkBlock",
- "aere.falcon.validatorCount"
- }) {
- System.clearProperty(p);
- }
- resetFalconSingleton();
- }
-
- // -------------------------------------------------------------------------------------------
- // 1. The deadlock state, stated as a property.
- // -------------------------------------------------------------------------------------------
-
- @Test
- public void restartedFleetIsNotArmedAndDoesNotHealWithTime() {
- final FalconSealSupport pqc = FalconSealSupport.instance();
-
- assertThat(pqc.lateAnchorPending())
- .describedAs(
- "fixture control: the late-anchor manifest must LOAD and stay PENDING, or every "
- + "assertion below is about a registry that was never configured")
- .isTrue();
- assertThat(pqc.lateAnchored()).isFalse();
- assertThat(pqc.lateAnchorFailed()).isFalse();
- assertThat(pqc.signingEnabled())
- .describedAs("fixture control: this node holds a Falcon key, so attachment is not off for "
- + "the trivial reason")
- .isTrue();
- assertThat(pqc.attachBlock()).isEqualTo(ATTACH);
-
- // This IS the post-restart state: the process has just started, the chain head is far past the
- // attachment height, and no block has been imported because nobody has proposed one.
- assertThat(pqc.attachmentArmed(HEAD))
- .describedAs(
- "the measured deadlock: attachment height long since passed, registry still pending, "
- + "so no seal is attached and no certificate can ever reach K")
- .isFalse();
-
- // And it does not heal. Time, and blocks that are never imported, change nothing.
- for (long n = HEAD; n <= HEAD + 10_000L; n += 1_000L) {
- assertThat(pqc.attachmentArmed(n))
- .describedAs("still not armed at height %s; nothing in the process flips it", n)
- .isFalse();
- }
- assertThat(pqc.registrySize())
- .describedAs("the registry is EMPTY while pending, which is why a seal cannot verify either")
- .isZero();
- }
-
- // -------------------------------------------------------------------------------------------
- // 2. The repair's mechanism: the SECOND caller, the one startup adds.
- // -------------------------------------------------------------------------------------------
-
- @Test
- public void activatingFromTheChainHeadArmsAttachment() {
- final FalconSealSupport pqc = FalconSealSupport.instance();
- assertThat(pqc.attachmentArmed(HEAD)).isFalse();
-
- // Exactly what the startup repair does: hand over the 32-byte value read from the anchor
- // contract's slot 0 in the CHAIN-HEAD world state. No block is imported anywhere here.
- final boolean activated = pqc.activateLateAnchor(onChainHash);
-
- assertThat(activated).isTrue();
- assertThat(pqc.lateAnchored()).isTrue();
- assertThat(pqc.lateAnchorPending()).isFalse();
- assertThat(pqc.registrySize()).isEqualTo(N);
- assertThat(pqc.addressBound())
- .describedAs("the activated registry must bind every index to an address, or a seal cannot "
- + "be resolved to a signer")
- .isTrue();
- assertThat(pqc.attachmentArmed(HEAD))
- .describedAs(
- "THE REPAIR: one activation from chain-head state arms attachment, so a restarted "
- + "validator emits Falcon-carrying Commits again, certificates reach K, and a "
- + "proposer can propose. This is the edge the deadlock needed and did not have.")
- .isTrue();
- }
-
- // -------------------------------------------------------------------------------------------
- // 3. THE NEGATIVE CONTROL. The repair must not have bought liveness by weakening the check.
- // -------------------------------------------------------------------------------------------
-
- @Test
- public void aWrongOnChainHashLeavesAttachmentOffAndIsTerminal() {
- final FalconSealSupport pqc = FalconSealSupport.instance();
-
- // One flipped nibble: a tampered anchor, or a wrong manifest shipped to this node.
- final char first = onChainHash.charAt(0);
- final String wrong = (first == '0' ? '1' : '0') + onChainHash.substring(1);
- assertThat(wrong).isNotEqualTo(onChainHash).hasSize(64);
-
- assertThat(pqc.activateLateAnchor(wrong))
- .describedAs("a mismatching anchor must NOT activate the registry")
- .isFalse();
- assertThat(pqc.lateAnchored()).isFalse();
- assertThat(pqc.lateAnchorFailed())
- .describedAs("and the mismatch must be TERMINAL, not merely 'not yet'")
- .isTrue();
- assertThat(pqc.registrySize())
- .describedAs("the registry stays EMPTY: fail-closed, not fail-open")
- .isZero();
- assertThat(pqc.attachmentArmed(HEAD))
- .describedAs(
- "attachment stays OFF after a failed activation. If this were true, the startup repair "
- + "would have turned a tamper detection into an arming path.")
- .isFalse();
-
- // And the correct hash afterwards does not resurrect it: a node that has seen a tampered anchor
- // stays refused, which is the same fail-closed rule the import path already had.
- assertThat(pqc.activateLateAnchor(onChainHash)).isFalse();
- assertThat(pqc.attachmentArmed(HEAD)).isFalse();
- }
-
- // -------------------------------------------------------------------------------------------
- // 4. Scope control: two callers now exist in one process.
- // -------------------------------------------------------------------------------------------
-
- @Test
- public void activationIsIdempotentAcrossRepeatedStartupCalls() {
- final FalconSealSupport pqc = FalconSealSupport.instance();
-
- assertThat(pqc.activateLateAnchor(onChainHash)).isTrue();
- final int afterFirst = pqc.registrySize();
-
- // The startup call has fired; the import path fires too, on the first block that arrives.
- assertThat(pqc.activateLateAnchor(onChainHash)).isTrue();
- assertThat(pqc.registrySize()).isEqualTo(afterFirst).isEqualTo(N);
- assertThat(pqc.attachmentArmed(HEAD)).isTrue();
-
- // Even a garbage hash after activation cannot un-arm it: activation is a one-way latch, so a
- // second reader with a stale view cannot disarm a fleet that is already sealing.
- assertThat(pqc.activateLateAnchor("00".repeat(32))).isTrue();
- assertThat(pqc.lateAnchorFailed()).isFalse();
- assertThat(pqc.attachmentArmed(HEAD)).isTrue();
- }
-
- // -------------------------------------------------------------------------------------------
- // 5. The rehearsal's OWN stimulus, replayed against THIS tree. Read the note before trusting it.
- // -------------------------------------------------------------------------------------------
-
- /**
- * The seven-node rehearsal ran a GENESIS-anchored registry, and the line it logged after the
- * simultaneous restart was the COVERAGE one: "no validator set has been observed yet, so registry
- * COVERAGE cannot be proven. Attachment stays OFF (fail-safe)". That condition does not exist in
- * this tree: {@code grep} for it returns nothing, because D-078 (2026-08-02) removed the fleet
- * question from the per-commit gate. The rehearsal binary was built from the 2026-08-01 tree,
- * which still had it.
- *
- * So this test states what is true HERE: a genesis-anchored node, freshly constructed, with no
- * validator set observed and no block imported, IS armed. The rehearsal's measured deadlock is
- * closed for the genesis-anchored path by a repair that already landed - and NOT by the startup
- * repair this class is about.
- *
- * Which is exactly why the startup repair is still needed: on the LATE-ANCHOR path, the one
- * the live chain must use because it cannot be re-genesised, {@code lateActivated} is still set
- * from one place only. Tests 1-3 measure that path.
- *
- * NOT MEASURED: that seven live nodes on a genesis-anchored network recover from a
- * simultaneous restart with a binary built from this tree.
- */
- @Test
- public void aGenesisAnchoredNodeIsArmedImmediatelyAfterRestart() throws Exception {
- System.clearProperty("aere.falcon.manifest");
- System.clearProperty("aere.falcon.anchor.address");
- System.clearProperty("aere.falcon.anchor.block");
- System.setProperty("aere.falcon.genesis", genesisPath.toAbsolutePath().toString());
- resetFalconSingleton();
-
- final FalconSealSupport pqc = FalconSealSupport.instance();
-
- assertThat(pqc.genesisAnchored())
- .describedAs("fixture control: the genesis manifest must verify against the anchored hash")
- .isTrue();
- assertThat(pqc.registrySize()).isEqualTo(N);
- assertThat(pqc.addressBound()).isTrue();
- assertThat(pqc.attachmentArmed(HEAD))
- .describedAs(
- "a genesis-anchored node arms with NO validator set observed and NO block imported. "
- + "The rehearsal's coverage condition is gone from this tree.")
- .isTrue();
- }
-
- private static void resetFalconSingleton() throws Exception {
- final Field f = FalconSealSupport.class.getDeclaredField("instance");
- f.setAccessible(true);
- f.set(null, null);
- }
-}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D141SealPersistenceTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D141SealPersistenceTest.java
deleted file mode 100644
index d66a209..0000000
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D141SealPersistenceTest.java
+++ /dev/null
@@ -1,621 +0,0 @@
-/*
- * 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.consensus.common.bft;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
-
-import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
-import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
-import org.hyperledger.besu.crypto.SecureRandomProvider;
-import org.hyperledger.besu.datatypes.Address;
-import org.hyperledger.besu.ethereum.ProtocolContext;
-import org.hyperledger.besu.ethereum.core.BlockHeader;
-import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
-
-import java.lang.reflect.Field;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.apache.tuweni.bytes.Bytes;
-import org.apache.tuweni.bytes.Bytes32;
-import org.bouncycastle.crypto.digests.KeccakDigest;
-import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
-import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-import org.mockito.quality.Strictness;
-
-/**
- * D-141. THE SECOND HALF OF THE FLEET-RESTART CHAIN DEATH: the heard seals themselves.
- *
- * MEASURED ON A NETWORK FIRST, NOT ASSUMED. With the anchor armed at K>0, a SIMULTANEOUS
- * restart of all seven validators kills the chain permanently (rehearsal
- * repetitie-activare-2026-08-05, isolated chain 330858). The FIRST half of that deadlock was the
- * registry, repaired the same day: it now activates at start-up from chain-head state, and all seven
- * nodes reported "activare a ancorei la PORNIRE din starea capului: REUSITA". The chain died anyway.
- * The refusal only changed shape, from "registry address-bound=false" to "registry
- * address-bound=TRUE ... Heard 0 seal(s)", frozen 150 s then 298 s.
- *
- * THE SECOND CIRCLE. The Falcon seals over M(head) travel on nothing but the Commit messages of
- * the head block, and those are never replayed after a restart. They exist nowhere else: the head's
- * own header carries a certificate over its PARENT, not over itself. So every node came back holding
- * zero seals, none could reach K, none could propose, and therefore none ever sent another Commit
- * for another node to hear. Seals come from Commits, Commits come from proposals, proposals need
- * seals.
- *
- * WHAT THIS CLASS MEASURES, one test per link, with the causal chain driven in BOTH directions so
- * that "refuses" is never satisfied by a producer that always refuses:
- *
- * NOT MEASURED here, and named so it is not read as covered: that seven live nodes recover from a
- * real simultaneous restart with a binary built from this tree. That needs the rehearsal network and
- * is separate evidence. This class measures every decision that recovery depends on.
- */
-public class D141SealPersistenceTest {
-
- /** Anchor activation height H. */
- private static final long H = 1_000L;
-
- /** Seal-attachment height, comfortably below H. */
- private static final long ATTACH = 900L;
-
- /** Height from which the staged threshold K is in force. */
- private static final long K_AT = H + 10L;
-
- /** The founder's decision of 2026-08-05: N=7 stays, and K=3 is the value with full margin. */
- private static final int K = 3;
-
- private static final int N = 7;
-
- private static final long CHAIN_ID = 2_800L;
-
- /** Measured block interval on the live chain, in milliseconds. */
- private static final long BLOCK_INTERVAL_MS = 523L;
-
- @TempDir private Path tmp;
-
- /** Stands in for the node's data directory, which is where the real path comes from. */
- private Path dataDirectory;
-
- private final List Three shapes of forgery are in the one file, because "a forged seal" is not one thing:
- *
- * WHAT THE WIRE BUYS, stated as the thing that is actually true. Before it, an ARMED node loaded
- * a v1 registry without a word, and the registry decides who a Falcon seal is credited to. Measured
- * on the real verification path on the same day: two rows with their public keys swapped - four
- * distinct keys, four distinct addresses, so no uniqueness check would see anything - produced an
- * ACCEPTED header; and the same key filed at two indices satisfied a threshold of two with one
- * private key, which makes the threshold itself fiction.
- *
- * WHY THE POSITIVE CONTROLS ARE THE EXPENSIVE HALF. A gate that refuses everything is not a gate,
- * it is an outage wearing a security message. The tests that cost the most to get right here are the
- * ones where the node STARTS: over a correct v2 registry, and over the very same v1 file when
- * nothing is armed.
- *
- * WHY THE ANCHOR CASE IS TESTED SEPARATELY FROM THE FORK-BLOCK CASE. They are different triggers
- * and only one of them was previously guarded at all. {@code armingReadinessDiagnostic()} returns
- * immediately when {@code aere.falcon.forkBlock} is unset, so AERE-PQC-REG-ARM-01 has never fired on
- * a node armed through the certificate anchor. This guard fires on both, and {@link
- * #armedThroughTheANCHORAloneTheNodeAlsoREFUSES} is the half that has no predecessor.
- *
- * WHAT IS NOT MEASURED HERE, written rather than implied: nothing is deployed, no node is
- * started, the fleet of seven is not touched, and every Falcon and ECDSA key below is a PROBE key
- * generated in this JVM. Whether the refusal behaves the same on the seven real boxes at a
- * coordinated restart is NOT MEASURED.
- */
-public class D146ArmingGateTest {
-
- /** The height at which this fixture arms Falcon blocking. */
- private static final long FORK = 7_000L;
-
- /** Attachment must lead the fork block; the same shape D079ForkArmingTest uses. */
- private static final long ATTACH = 6_000L;
-
- /** The chain id the registry is bound to. Not 2800: nothing here may look like the live fleet. */
- private static final long CHAIN_ID = 220_878L;
-
- /** The height the binding proofs are signed for. */
- private static final long BIND_HEIGHT = FORK;
-
- private static final int N = 4;
-
- /** Every property this class is allowed to touch. Cleared before AND after every test. */
- private static final List This is the case with no predecessor. AERE-PQC-REG-ARM-01 is raised by {@code
- * armingReadinessDiagnostic()}, whose first statement is to return when the fork block is unset,
- * so an anchor-armed node has never been asked ANY question about its registry's shape at startup.
- */
- @Test
- public void armedThroughTheANCHORAloneTheNodeAlsoREFUSES() throws Exception {
- final Path v1 = writeRegistry("registru-v1.properties", false, false);
- System.setProperty("aere.falcon.registry", v1.toAbsolutePath().toString());
- armWithAnchorOnly();
-
- assertThat(System.getProperty("aere.falcon.forkBlock"))
- .describedAs("this test is only worth something while the fork block is genuinely unset")
- .isNull();
- assertThatThrownBy(FalconSealSupport::instance)
- .isInstanceOf(PqRegistryHash.RegistryConfigException.class)
- .hasMessageContaining("AERE-PQC-REG-ARM-02");
- }
-
- // -------------------------------------------------------------------------------------------
- // 2. THE POSITIVE CONTROLS. Without these the refusals above could be a load bug.
- // -------------------------------------------------------------------------------------------
-
- /**
- * The same node, the same arming, over a registry whose every row carries a Falcon possession
- * proof and an ECDSA claim signed by that row's own validator key, STARTS - and loads.
- */
- @Test
- public void armedOverAV2RegistryTheNodeSTARTS() throws Exception {
- armWithForkBlock(writeRegistry("registru-v2.properties", true, true));
-
- assertThatCode(FalconSealSupport::instance)
- .describedAs(
- "POSITIVE CONTROL: the gate can be green. A refusal that no correct input can pass is "
- + "an outage wearing a security message")
- .doesNotThrowAnyException();
- assertThat(FalconSealSupport.instance().registrySize())
- .describedAs("and it must really have loaded the file, not merely declined to throw")
- .isEqualTo(N);
- }
-
- /** The same, armed through the anchor alone. */
- @Test
- public void armedThroughTheANCHORAloneOverAV2RegistryTheNodeSTARTS() throws Exception {
- final Path v2 = writeRegistry("registru-v2.properties", true, true);
- System.setProperty("aere.falcon.registry", v2.toAbsolutePath().toString());
- armWithAnchorOnly();
-
- assertThatCode(FalconSealSupport::instance).doesNotThrowAnyException();
- assertThat(FalconSealSupport.instance().registrySize()).isEqualTo(N);
- }
-
- // -------------------------------------------------------------------------------------------
- // 3. THE BOUNDARY. A node that arms NOTHING must be untouched by any of this.
- // -------------------------------------------------------------------------------------------
-
- /**
- * THE GUARANTEE FOR CHAIN 2800 AS IT STANDS: a node with no {@code aere.pq.*} property and no
- * {@code aere.falcon.forkBlock} starts over the very same v1 file that is refused when armed.
- *
- * The assertion that carries the weight is not the "starts" - it is the property sweep. A test
- * that only asserted "does not throw" would keep passing if a later edit made the guard read some
- * other property that happened to be set in this JVM. The sweep states the precondition as a
- * measurement: at the moment the constructor runs, NO system property beginning with {@code
- * aere.pq.} exists, and neither does the fork block.
- */
- @Test
- public void withNothingArmedTheGateIsInertOverTheSameV1Registry() throws Exception {
- final Path v1 = writeRegistry("registru-v1.properties", false, false);
- System.setProperty("aere.falcon.registry", v1.toAbsolutePath().toString());
-
- assertThat(systemPropertiesStartingWith("aere.pq."))
- .describedAs("the precondition of this test, measured rather than assumed")
- .isEmpty();
- assertThat(System.getProperty("aere.falcon.forkBlock")).isNull();
-
- assertThatCode(FalconSealSupport::instance)
- .describedAs(
- "the same file that is refused when armed is accepted when nothing is armed, so the "
- + "trigger is ARMING and not the file")
- .doesNotThrowAnyException();
- assertThat(FalconSealSupport.instance().registrySize())
- .describedAs("and an unarmed node's registry is loaded exactly as it was before D-146")
- .isEqualTo(N);
- }
-
- /**
- * The same boundary with NO registry configured either, which is a node holding nothing at all -
- * the shape of a fresh box joining the fleet before any key ceremony.
- */
- @Test
- public void aNodeWithNoFalconConfigurationAtAllStarts() {
- assertThat(systemPropertiesStartingWith("aere.pq.")).isEmpty();
- assertThat(System.getProperty("aere.falcon.registry")).isNull();
-
- assertThatCode(FalconSealSupport::instance).doesNotThrowAnyException();
- }
-
- /**
- * An ARMED node with no registry file at all is deliberately NOT this guard's business, and this
- * test is what stops that from being a silent decision.
- *
- * D-146 is mis-ATTRIBUTION, which needs rows; an empty registry credits nobody. The condition
- * is owned by AERE-PQC-CFG-UNSAFE-08 when the threshold is positive, and MEASURED here: with a
- * threshold of zero, which is the warm-up regime the fleet is meant to arm INTO, the node starts.
- * An earlier revision of this guard refused here, and the cost was exactly that - the intended
- * activation procedure became unstartable.
- */
- @Test
- public void armedWithNoRegistryAtAllAndAZeroThresholdTheNodeStarts() {
- System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(FORK));
- System.setProperty(PqAnchorConfig.PROPERTY_CHAIN_ID, Long.toString(CHAIN_ID));
- System.setProperty(PqAnchorConfig.PROPERTY_MIN_SEALS, FORK + ":0");
- System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
- System.setProperty("aere.falcon.testnetAllowSmallFleet", "true");
-
- assertThatCode(FalconSealSupport::instance)
- .describedAs("K=0 over an empty registry is the warm-up regime, not a D-146 defect")
- .doesNotThrowAnyException();
- }
-
- // -------------------------------------------------------------------------------------------
- // 4. HALF A v2 REGISTRY IS NOT A v2 REGISTRY.
- // -------------------------------------------------------------------------------------------
-
- /**
- * A row that carries a Falcon possession proof and no ECDSA claim proves that SOMEBODY holds the
- * key, and says nothing about which validator asked for it - which is the whole of D-146.
- *
- * MEASURED, and the assertion was CHANGED to match the measurement rather than the other way
- * round. The expectation written first was AERE-PQC-REG-ARM-02. What actually happens is a refusal
- * one step EARLIER, at load, with AERE-PQC-REG-LOAD-21, because the loader counts proofs against
- * claims and refuses a half-bound file before the arming gate ever sees it. That is the stronger
- * of the two refusals - it holds whether or not the node is armed - so this is asserted on the
- * code that actually fires.
- */
- @Test
- public void possessionWithoutAClaimIsRefusedEarlierStillAtLoad() throws Exception {
- armWithForkBlock(writeRegistry("registru-doar-posesie.properties", true, false));
-
- assertThatThrownBy(FalconSealSupport::instance)
- .describedAs(
- "the attacker is the key holder, so a genuine possession proof over a lying row is "
- + "genuinely produceable; only the validator's own signature closes it")
- .isInstanceOf(PqRegistryHash.RegistryConfigException.class)
- .hasMessageContaining("AERE-PQC-REG-LOAD-21");
- }
-
- // -------------------------------------------------------------------------------------------
- // Helpers.
- // -------------------------------------------------------------------------------------------
-
- /** Arm through {@code aere.falcon.forkBlock}, the trigger AERE-PQC-REG-ARM-01 also watches. */
- private void armWithForkBlock(final Path registry) {
- System.setProperty("aere.falcon.registry", registry.toAbsolutePath().toString());
- System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
- System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
- System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
- // N=4 is below the blocking minimum; this fixture is an isolated network and says so with the
- // switch the codebase already uses for exactly that, rather than by pretending to be seven.
- System.setProperty("aere.falcon.testnetAllowSmallFleet", "true");
- }
-
- /**
- * Arm through the CERTIFICATE ANCHOR only, leaving {@code aere.falcon.forkBlock} unset. The
- * threshold is 2, which {@code worstCaseKeyedSigners(4, 4)} = 3 guarantees, so the D-078 guard
- * next door stays silent and cannot be mistaken for this one.
- */
- private void armWithAnchorOnly() {
- System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(FORK));
- System.setProperty(PqAnchorConfig.PROPERTY_CHAIN_ID, Long.toString(CHAIN_ID));
- System.setProperty(PqAnchorConfig.PROPERTY_MIN_SEALS, FORK + ":0," + (FORK + 10L) + ":2");
- System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
- System.setProperty("aere.falcon.testnetAllowSmallFleet", "true");
- }
-
- /**
- * Write a registry in the legacy properties form. {@code withPossession} and {@code withClaim} are
- * separate so that the half-bound case can be built, which is the one the loader must refuse.
- */
- private Path writeRegistry(
- final String name, final boolean withPossession, final boolean withClaim) throws Exception {
- final StringBuilder b = new StringBuilder();
- if (withPossession || withClaim) {
- b.append("formatVersion=").append(PqRegistryBinding.FORMAT_VERSION).append('\n');
- b.append("chainId=").append(CHAIN_ID).append('\n');
- b.append("bindHeight=").append(BIND_HEIGHT).append('\n');
- }
- b.append("count=").append(N).append('\n');
- for (int i = 0; i < N; i++) {
- b.append(i).append('=').append(unprefixed(PqV2Fixture.publicKey(i))).append('\n');
- b.append(i)
- .append(".addr=")
- .append(unprefixed(PqV2Fixture.address(i).getBytes().toArray()))
- .append('\n');
- if (withPossession) {
- b.append(i)
- .append(".pop=")
- .append(strip(PqV2Fixture.popHex(CHAIN_ID, BIND_HEIGHT, N, i)))
- .append('\n');
- }
- if (withClaim) {
- b.append(i)
- .append(".claim=")
- .append(strip(PqV2Fixture.claimHex(CHAIN_ID, BIND_HEIGHT, N, i)))
- .append('\n');
- }
- }
- final Path f = tmp.resolve(name);
- Files.writeString(f, b.toString(), StandardCharsets.UTF_8);
- return f;
- }
-
- private static String unprefixed(final byte[] b) {
- return Bytes.wrap(b).toUnprefixedHexString();
- }
-
- private static String strip(final String hex) {
- return hex.startsWith("0x") ? hex.substring(2) : hex;
- }
-
- /** Every system property name with the given prefix, so a precondition can be MEASURED. */
- private static List MEASURED 2026-08-06, and it is the reason this method exists rather than being assumed
- * unnecessary. {@code PqAnchorProducer.config()} memoises the first configuration it ever builds,
- * for the life of the JVM. That is CORRECT in production - a node is one JVM with one set of
- * properties, and a configuration that could change underneath the consensus path would be worse
- * than one that cannot. In a test JVM shared by every class in this module it means an anchor
- * armed by an earlier test is still armed here, and {@link
- * #withNothingArmedTheGateIsInertOverTheSameV1Registry} failed exactly that way before this call
- * was added: the property sweep found no {@code aere.pq.*} and the node still refused, because
- * the memo held another class's anchor.
- */
- private static void forgetAnchorConfig() {
- org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.useConfigForTesting(
- null);
- }
-
- private static void resetFalconSingleton() throws Exception {
- final Field f = FalconSealSupport.class.getDeclaredField("instance");
- f.setAccessible(true);
- f.set(null, null);
- }
-}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D147InertBinaryTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D147InertBinaryTest.java
deleted file mode 100644
index 68737f3..0000000
--- a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D147InertBinaryTest.java
+++ /dev/null
@@ -1,466 +0,0 @@
-/*
- * 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.consensus.common.bft;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
-
-import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
-import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
-import org.hyperledger.besu.datatypes.Address;
-import org.hyperledger.besu.ethereum.ProtocolContext;
-import org.hyperledger.besu.ethereum.core.BlockHeader;
-import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Optional;
-import java.util.Properties;
-import java.util.stream.Collectors;
-
-import org.apache.logging.log4j.Level;
-import org.apache.tuweni.bytes.Bytes32;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.quality.Strictness;
-
-/**
- * THE COMPATIBILITY PROPERTY, which is the one that decides whether any of this can be shipped.
- *
- * The three anchor patches plus the D-146 arming gate are meant to travel onto the seven live
- * boxes BEFORE the activation height, so that the fleet is already running the binary when the
- * height arrives and activation is a restart-free event. That plan is only sound if a node holding
- * this binary and NO {@code aere.pq.*} configuration is indistinguishable from one holding the
- * binary it replaces: it must start, it must produce blocks, and it must not say a word about an
- * anchor that is not armed. If that property is lost, the whole package is unusable regardless of
- * how correct the anchor logic is, because it could not be staged.
- *
- * WHY THE SILENCE IS MEASURED AND NOT ASSUMED. "It returns early, so it cannot log" is a reading
- * of the code, not a measurement, and the integrated tree has four patches whose log sites nobody
- * has looked at together. Here the actual Log4j2 pipeline is tapped and the lines are counted.
- *
- * WHY {@link #positiveControlTheCaptorSEESTheAnchorWhenItISArmed} is not optional. A captor that
- * attaches to nothing reports silence forever, and every assertion in {@link
- * #withNoAerePropertiesTheProposerProducesABlockAndSaysNOTHING} would pass against a broken tap.
- * The positive control arms the anchor and requires that the SAME captor, in the same JVM, sees the
- * producer's activation line. Without it this class would be a proof that cannot go red.
- *
- * WHY THE CAPTOR IS BUILT BY REFLECTION. {@code log4j-core}, which owns the appender API, is on
- * this module's RUNTIME test classpath but not its COMPILE one - measured, not assumed. Adding it as
- * a compile dependency would put a build file into the AERE overlay, which until now is Java only.
- * Reflection keeps the overlay unchanged, and the positive control is what makes it safe: if any of
- * the reflective steps silently failed, the captor would see nothing and the positive control would
- * be the test that fails.
- *
- * NOT MEASURED, and written rather than implied: nothing is deployed and no node is started. That
- * an unarmed node on one of the seven real boxes behaves this way over a real chain, at 523 ms
- * blocks, alongside a peer that IS armed, is NOT MEASURED and needs the rehearsal network.
- */
-public class D147InertBinaryTest {
-
- /**
- * Loggers that exist ONLY because of the anchor work, so any line from them on an unarmed node is
- * by itself a finding.
- *
- * {@code FalconSealSupport} is deliberately NOT here even though it is the loudest of them.
- * It predates the anchor and legitimately says one thing at startup; listing it would make the
- * filter report a four-year-old INFO line as new anchor chatter. Its armed messages are caught by
- * {@link #ANCHOR_WORDS} instead, which keys on what the line SAYS rather than who said it.
- */
- private static final List MEASURED 2026-08-06, and it is the reason this constant exists rather than an {@code
- * isEmpty()} on everything. The first shape of this test asserted total silence and went red on
- * this line. It is not a regression: {@code git log -S} places it in commit 307fd0d0, the snapshot
- * of everything built between 14 June and 2 August, so it predates all three anchor patches and
- * the arming gate. It is {@code LOG.info} and it says the node has no Falcon registry, which is
- * true and was equally true of the binary being replaced.
- *
- * So the property that is actually worth defending is not "says nothing" - that was never true
- * - but "says nothing NEW, and nothing about the anchor". Pinning the exact text is what makes the
- * second half enforceable: a fourth patch that adds one more startup line has to come here and
- * change this constant deliberately.
- */
- private static final String THE_ONE_PRE_EXISTING_LINE =
- "AERE PQC: no Falcon registry configured "
- + "(aere.falcon.genesis/aere.falcon.manifest/aere.falcon.registry); "
- + "hybrid seal verification will be a no-op.";
-
- private static final long CHAIN_ID = 220_878L;
-
- private static final long H = 4_000L;
-
- /**
- * Every property this class may touch. The unarmed test does not rely on this list - it sweeps the
- * whole property table - but the armed one must put back exactly what it took.
- */
- private static final List The block-production half is asserted on OBJECT IDENTITY, not equality. {@code
- * PqAnchorProducer.apply} returns its argument unchanged at the first branch when the anchor is
- * not active; an equal-but-rebuilt {@code BftExtraData} would mean the producer had walked the
- * certificate path and merely arrived back at the same value, which is a different and much
- * weaker statement.
- */
- @Test
- public void withNoAerePropertiesTheProposerProducesABlockAndSaysNOTHING() throws Exception {
- // The precondition is MEASURED over the whole property table rather than trusted to the
- // teardown of whatever test ran before this one in this JVM.
- assertThat(systemPropertiesStartingWith("aere."))
- .describedAs("the precondition of this test, measured rather than assumed")
- .isEmpty();
-
- final LogCaptor captor = LogCaptor.attach();
- final BftExtraData produced;
- final BftExtraData base = plainExtraData();
- try {
- assertThatCode(FalconSealSupport::instance)
- .describedAs("a box with no key ceremony behind it must still come up")
- .doesNotThrowAnyException();
-
- final BlockHeader parent = new BlockHeaderTestFixture().number(H + 500L).buildHeader();
- produced = PqAnchorProducer.apply(base, parent, contextWith(List.of()));
- } finally {
- captor.detach();
- }
-
- assertThat(produced)
- .describedAs(
- "the unarmed producer must hand back the very object it was given; an equal copy would "
- + "mean it had walked the certificate path")
- .isSameAs(base);
-
- assertThat(PqAnchorProducer.config().everActive())
- .describedAs("and it must consider itself never-active, not merely inactive right now")
- .isFalse();
-
- assertThat(captor.anchorLines())
- .describedAs(
- "an operator staging this binary before the height must see NOTHING about the anchor; "
- + "%d line(s) in total were seen, so the captor was live",
- captor.total())
- .isEmpty();
-
- // And nothing NEW of any kind. This is the half that catches a future patch adding chatter.
- assertThat(captor.aereLines())
- .describedAs(
- "the whole AERE output of an unarmed node, pinned: exactly the one INFO line that "
- + "predates these patches (commit 307fd0d0). A new line here is a staging "
- + "regression even when it is harmless, because it changes what the fleet prints "
- + "on a restart that is supposed to be a no-op.")
- .containsExactly(THE_ONE_PRE_EXISTING_LINE);
- }
-
- // ---------------------------------------------------------------------------------------------
- // THE POSITIVE CONTROL, without which the test above proves nothing.
- // ---------------------------------------------------------------------------------------------
-
- /**
- * The same captor, the same JVM, the same loggers - with the anchor armed. If this does not see a
- * line, the silence measured above is the silence of a broken tap and means nothing.
- *
- * The line chosen is the producer's own activation notice, emitted from {@code
- * PqAnchorProducer.config()} the first time a configuration is built. Its once-per-JVM latch is
- * reset by {@code useConfigForTesting(null)}, which is why {@link #forgetAnchorConfig()} runs
- * before every test in this class.
- */
- @Test
- public void positiveControlTheCaptorSEESTheAnchorWhenItISArmed() throws Exception {
- System.setProperty(PqAnchorConfig.PROPERTY_ANCHOR_BLOCK, Long.toString(H));
- System.setProperty(PqAnchorConfig.PROPERTY_CHAIN_ID, Long.toString(CHAIN_ID));
- // A whole-zero schedule is refused since the D-147 floor (armed anchor, no signature
- // requirement, for ever). The warm-up step at H stays 0; the rise satisfies the floor.
- System.setProperty(PqAnchorConfig.PROPERTY_MIN_SEALS, H + ":0," + (H + 21_600L) + ":3");
-
- final LogCaptor captor = LogCaptor.attach();
- try {
- PqAnchorProducer.config();
- } finally {
- captor.detach();
- }
-
- assertThat(captor.anchorLines())
- .describedAs(
- "the captor must be able to hear the anchor, or the silence next door is worthless")
- .isNotEmpty();
- assertThat(String.join("\n", captor.anchorLines())).contains("producer armed");
- }
-
- // ---------------------------------------------------------------------------------------------
- // Helpers.
- // ---------------------------------------------------------------------------------------------
-
- /** Extra data with no anchor digest, i.e. exactly what a pre-fork proposer builds. */
- private static BftExtraData plainExtraData() {
- return new BftExtraData(
- Bytes32.ZERO,
- Collections.emptyList(),
- Optional.empty(),
- 0,
- Collections.emptyList(),
- Collections.emptyList());
- }
-
- private static ProtocolContext contextWith(final Collection {@link #attach()} throws if any reflective step fails. It does NOT fall back to a silent
- * captor: a captor that quietly captures nothing is precisely the failure this class is written to
- * exclude.
- */
- private static final class LogCaptor {
-
- private final List WHAT THE FIRST SHAPE DID, MEASURED AND NOT ARGUED. On 2026-08-06 hardening (b) refused every
- * unbound height at or above the arming height, deciding from the block NUMBER alone. Run against
- * the suite that gave 588 tests and 0 failures on a clean tree, it gave 597 tests and 6 failures:
- * five in {@code D141SealPersistenceTest} and one in {@code D078ValidatorSetChangeTest}. Both
- * classes work on THIS NODE'S OWN head - restarting and re-reading its own seal file, and proposing
- * on top of its own head - and in all six the number handed to the guard was 1030 with an arming
- * height of 1000. A genuinely historical question, in the same process in the same second, hands
- * the guard exactly those numbers too. No arithmetic on the height separates them.
- *
- * THE OPERATIONAL CONSEQUENCE, in the words of the D078 failure itself: {@code refusing to
- * propose on top of block 1030 because this node holds 0 valid eligible Falcon seal(s)}. The first
- * shape turned a defect that is invisible on a running fleet and fatal only to a node syncing later
- * into one that stops block production on all seven, in the minute the anchor is armed.
- *
- * WHAT SEPARATES THEM IS WHO SUPPLIES THE SUBJECT, and that is known at every call site and was
- * being discarded at the interface boundary. So {@code PqSignerRegistry} now carries two named
- * pairs, and the compiler forces every call site to say which question it is asking. This class is
- * the proof that the two doors answer DIFFERENTLY at the SAME height, that the own-head door is not
- * a loophole, and that the history door still refuses.
- *
- * THIS CLASS CANNOT GO GREEN BY ACCIDENT. Three of its tests fail if the own-head door is made
- * to refuse (which is the first shape restored), and three fail if the history door is made to
- * answer (which is the pre-2026-08-06 defect restored). The two plants are run in opposite
- * directions and both are recorded in the evidence directory.
- */
-public class D2CallerIntentTest {
-
- /** Anchor activation height H, matching the fixture the six failures ran under. */
- private static final long H = 1_000L;
-
- /** Height from which the staged threshold is non-zero, i.e. the fully armed regime. */
- private static final long K_AT = H + 10L;
-
- /**
- * The height the six failures actually presented to the guard: this node's own head, above the
- * arming height. Named for what it is, because the whole point is that the NUMBER is innocent.
- */
- private static final long OWN_HEAD = 1_030L;
-
- /** A height far above H, standing in for "a year of history above the arming height". */
- private static final long DEEP = K_AT + 5_000L;
-
- private static final int N = 7;
-
- private static final long CHAIN_ID = 220_878L;
-
- @TempDir private Path tmp;
-
- private final List WHAT THE DOSSIER MEASURED. {@code PqSignerRegistry} had {@code addressForIndex(int)} and {@code
- * verify(int, Bytes, Bytes)} with no height, and {@code FalconSealSupport} held ONE registry loaded
- * at start-up. So a header that passed both anchor rules was REJECTED the moment index 0's Falcon
- * key was rotated - same header, same parent, same validator set.
- *
- * WHAT WAS REPAIRED BEFORE THIS FILE, AND WHAT WAS NOT. Commit f3ebe90c (D-081) gave the
- * validation path {@code addressForIndexAt} / {@code verifyAt} and a height-indexed schedule. The
- * measurement of 2026-08-05 found the repair INERT, for a reason that is one line long: with no
- * {@code config.pqRegistryHash} in genesis - and there is none in any genesis this fleet runs -
- * {@code keyAt} fell back to the registry in force AT THE HEAD, at every height. Height-aware
- * signatures, head-registry answers. T2 stood exactly as measured.
- *
- * WHAT THIS FILE ASSERTS, as a property and not as a scenario: at and above the arming height,
- * a node that cannot say which key set was in force must REFUSE, not guess. Below the arming
- * height it must keep answering from the head registry, because nothing there is being judged and
- * the 11.8 million blocks already on chain 2800 must behave bit for bit as they did.
- *
- * THE NEGATIVE CONTROL IS BUILT IN, not promised. {@link
- * #belowTheArmingHeightTheHeadRegistryStillAnswers()} fails if the refusal is made unconditional;
- * {@link #whenTheAnchorIsNotArmedNOTHINGCHANGES()} fails if it is made independent of arming; {@link
- * #withTheScheduleConfiguredTheArmedHeightsAnswerAgain()} fails if the refusal is anything other
- * than a missing height-to-registry binding. And the measurement itself, {@link
- * #d2t2AtAndAboveTheArmingHeightWithNoScheduleTheAnswerIsRefusal()}, is GREEN on the unrepaired code
- * only if the fallback is restored - which is exactly the one-line edit the repair removed.
- */
-public class D2RegistryHeightRefusalTest {
-
- /** Anchor activation height H: from here a header's Falcon certificate carries weight. */
- private static final long H = 1_000L;
-
- /** Height from which the staged threshold is non-zero, i.e. the fully armed regime. */
- private static final long K_AT = H + 10L;
-
- /** A height far above H, standing in for "a year of history above the arming height". */
- private static final long DEEP = K_AT + 5_000L;
-
- private static final int N = 7;
-
- private static final long CHAIN_ID = 220_878L;
-
- @TempDir private Path tmp;
-
- private final List DATED 2026-08-31: the fork pair was consolidated -- the twin classes were one copy too many,
+ * and the divergence above is precisely what duplication costs. The reproduction now runs the
+ * lifecycle of the surviving class, {@code PqForkThresholdReachabilityTest}, which has carried the
+ * cleanup line since 2026-08-11; the assertion is unchanged.
*/
public class PqAnchorProducerCacheHygieneTest {
@@ -75,8 +80,8 @@ public class PqAnchorProducerCacheHygieneTest {
@Test
public void theReachabilitySequenceLeavesNoArmedAnchorBehind() throws Exception {
- final D078ThresholdReachabilityTest vinovat = new D078ThresholdReachabilityTest();
- final Field tmpField = D078ThresholdReachabilityTest.class.getDeclaredField("tmp");
+ final PqForkThresholdReachabilityTest vinovat = new PqForkThresholdReachabilityTest();
+ final Field tmpField = PqForkThresholdReachabilityTest.class.getDeclaredField("tmp");
tmpField.setAccessible(true);
tmpField.set(vinovat, tmp);
@@ -91,7 +96,7 @@ public class PqAnchorProducerCacheHygieneTest {
assertThat(PqAnchorProducer.config().everActive())
.describedAs(
- "after D078ThresholdReachabilityTest's own teardown, a config built in this JVM must "
+ "after PqForkThresholdReachabilityTest's own teardown, a config built in this JVM must "
+ "not claim an armed anchor; if it does, the per-JVM cache survived the cleanup "
+ "and every proof-less fixture in the next class dies with AERE-PQC-REG-ARM-02")
.isFalse();
diff --git a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D078GateFeedTest.java b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D078GateFeedTest.java
deleted file mode 100644
index f016124..0000000
--- a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D078GateFeedTest.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * 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.consensus.qbft.headervalidationrules;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.CHAIN_ID;
-import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.H;
-import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.VALIDATORS;
-import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.parentHeader;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
-
-import org.hyperledger.besu.consensus.common.bft.BftContext;
-import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
-import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
-import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
-import org.hyperledger.besu.datatypes.Address;
-import org.hyperledger.besu.ethereum.ProtocolContext;
-import org.hyperledger.besu.ethereum.core.BlockHeader;
-
-import java.lang.reflect.Field;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.OptionalInt;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.quality.Strictness;
-
-/**
- * D-078, THE OTHER HALF: who feeds the seal-attachment gate above the anchor height.
- *
- * The gate's registry-coverage report reads a validator set recorded by {@code
- * FalconSealSupport.observeValidators}. Until 2026-08-02 the ONLY caller of that method was {@link
- * FalconSealValidationRule}, and {@link PqAnchorConfig#legacyFalconRuleRetirementBlock()} stands
- * that rule down at exactly the anchor height H. So from H upward nothing fed it: the recorded set
- * was either frozen at a height below H, or - on any node whose process started above H - never
- * recorded at all. The old gate answered "never recorded" by switching seal attachment off, which
- * is one restart away from a chain that cannot propose.
- *
- * This class measures the WIRING, in both directions, and it is the only thing that separates the
- * repair from a claim about it:
- *
- * {@code FalconSealSupport.validateAnchorObservationHeightOrAbort} refuses to start a node whose
- * blocking height is armed at or before the height at which its registry can become active. That
- * closes the misconfiguration. It cannot close the ACCIDENT: an operator declares the observation
- * height correctly, the anchor-deploy transaction does not land, and the blocking height arrives
- * over an empty registry anyway.
- *
- * {@link FalconSealValidationRule} answers that by staying LOG-ONLY, and that answer is right -
- * blocking over an empty registry buys no safety and costs the chain. It is also exactly how the
- * condition used to disappear: the node was configured to enforce a post-quantum quorum, enforced
- * nothing, and said so in a warning that nothing reads and no command can exit on.
- *
- * These three tests measure the mark it now leaves, in both directions.
- */
-public class D079ArmedWithoutRegistryTest {
-
- private static final int N = 9;
-
- private static final long OBSERVE = 5_000L;
-
- private static final long ATTACH = 6_000L;
-
- private static final long FORK = 7_000L;
-
- private static final String ANCHOR_ADDRESS = "0x0000000000000000000000000000000000000fa1";
-
- /** AERE D-146: the chain this fixture's registry is BOUND to. Inside every proof, so stated. */
- private static final long CHAIN_ID = 2_800L;
-
- @TempDir private Path tmp;
-
- @BeforeEach
- public void setUp() throws Exception {
- // AERE D-146 (2026-08-06): v2, proof-bound, bound at FORK. This manifest used to spell its
- // addresses 0xC00+i; no secp256k1 key produces those, so once AERE-PQC-REG-ARM-02 was wired
- // this armed fixture could not start at all. PqV2Fixture lives in consensus:common's test
- // source set and reaches here through the testArtifacts dependency this module already had.
- final StringBuilder m = new StringBuilder("{");
- m.append(PqV2Fixture.manifestHeader(N, CHAIN_ID, FORK));
- for (int i = 0; i < N; i++) {
- m.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, FORK));
- }
- m.append("}");
- final Path manifest = tmp.resolve("falcon-late-manifest.json");
- Files.writeString(manifest, m.toString());
-
- System.setProperty("aere.falcon.manifest", manifest.toAbsolutePath().toString());
- System.setProperty("aere.falcon.anchor.address", ANCHOR_ADDRESS);
- System.setProperty("aere.falcon.anchor.block", Long.toString(OBSERVE));
- System.setProperty("aere.falcon.attachBlock", Long.toString(ATTACH));
- System.setProperty("aere.falcon.forkBlock", Long.toString(FORK));
- System.setProperty("aere.falcon.validatorCount", Integer.toString(N));
- forgetFalconSingleton();
- }
-
- @AfterEach
- public void tearDown() throws Exception {
- for (final String p :
- new String[] {
- "aere.falcon.manifest",
- "aere.falcon.anchor.address",
- "aere.falcon.anchor.block",
- "aere.falcon.attachBlock",
- "aere.falcon.forkBlock",
- "aere.falcon.validatorCount"
- }) {
- System.clearProperty(p);
- }
- forgetFalconSingleton();
- }
-
- @Test
- public void belowTheBlockingHeightNothingIsRecorded() {
- // The negative control. A counter that were set unconditionally would pass the test below and
- // mean nothing at all.
- final FalconSealValidationRule rule = new FalconSealValidationRule(Long.MAX_VALUE);
- final BlockHeader parent = parentHeader(FORK - 2L);
- final BlockHeader block = parentHeader(FORK - 1L);
-
- assertThat(rule.validate(block, parent, contextWith(VALIDATORS))).isTrue();
- assertThat(FalconSealSupport.instance().blockingArmedWithoutRegistrySince())
- .describedAs("below the blocking height there is nothing inert about being log-only")
- .isEqualTo(-1L);
- }
-
- @Test
- public void atTheBlockingHeightWithNoActiveRegistryTheHeightIsRecorded() {
- final FalconSealValidationRule rule = new FalconSealValidationRule(Long.MAX_VALUE);
- final BlockHeader parent = parentHeader(FORK - 1L);
- final BlockHeader block = parentHeader(FORK);
-
- assertThat(FalconSealSupport.instance().blockingArmedWithoutRegistrySince()).isEqualTo(-1L);
- assertThat(rule.validate(block, parent, contextWith(VALIDATORS)))
- .describedAs(
- "the rule must still ACCEPT: blocking over an empty registry is a halt, not a safeguard")
- .isTrue();
- assertThat(FalconSealSupport.instance().blockingArmedWithoutRegistrySince())
- .describedAs(
- "the node is configured to enforce a Falcon quorum at %d and is enforcing nothing. That "
- + "must be a value something can read, not a line in a file.",
- FORK)
- .isEqualTo(FORK);
- assertThat(FalconSealSupport.instance().anchorObserveBlock())
- .describedAs("and the declared height it was measured against must be readable too")
- .isEqualTo(OBSERVE);
- }
-
- @Test
- public void theRecordedHeightIsTheFirstOneAndDoesNotMoveWithTheChain() {
- final FalconSealValidationRule rule = new FalconSealValidationRule(Long.MAX_VALUE);
- rule.validate(parentHeader(FORK), parentHeader(FORK - 1L), contextWith(VALIDATORS));
- rule.validate(parentHeader(FORK + 40L), parentHeader(FORK + 39L), contextWith(VALIDATORS));
-
- assertThat(FalconSealSupport.instance().blockingArmedWithoutRegistrySince())
- .describedAs(
- "the value answers 'since when', so a later block must not overwrite it; if it tracked "
- + "the head it would report a fresh problem forever and never a duration")
- .isEqualTo(FORK);
- }
-
- private static ProtocolContext contextWith(final Collection
- *
- *
- *
- *
- *
- *
- *
- */
- @Test
- public void aForgedSealInTheFileIsRejectedAtReadAndNeverEntersTheCache() throws Exception {
- final List
- *
- */
-public class D078GateFeedTest {
-
- private static final PqAnchorConfig ARMED =
- new PqAnchorConfig(CHAIN_ID, H, Map.of(H, 0), OptionalInt.empty(), false);
-
- @BeforeEach
- public void resetSingleton() throws Exception {
- forgetFalconSingleton();
- }
-
- @AfterEach
- public void resetSingletonAfter() throws Exception {
- forgetFalconSingleton();
- }
-
- @Test
- public void theLegacyRuleStandsDownAtTheAnchorHeightSoItCannotBeTheFeed() {
- assertThat(ARMED.legacyFalconRuleRetirementBlock())
- .describedAs("the legacy Falcon rule retires at exactly the anchor height")
- .isEqualTo(H);
-
- final FalconSealValidationRule legacy =
- new FalconSealValidationRule(ARMED.legacyFalconRuleRetirementBlock());
- final BlockHeader parent = parentHeader(H - 1L);
- final BlockHeader atH = parentHeader(H);
-
- assertThat(legacy.validate(atH, parent, contextWith(VALIDATORS)))
- .describedAs("retired, so it accepts without doing anything")
- .isTrue();
- assertThat(FalconSealSupport.instance().observedValidatorsHeight())
- .describedAs(
- "at and above the anchor height the legacy rule records NOTHING. That is correct for the "
- + "rule and fatal for anything that depended on it as its only source.")
- .isEqualTo(-1L);
- }
-
- @Test
- public void theAnchorSealsRuleFeedsTheGateAtEveryHeightFromH() {
- final PqAnchorSealsRule rule = new PqAnchorSealsRule(ARMED, new NoRegistry());
- final BlockHeader parent = parentHeader(H + 40L);
- final BlockHeader block = PqAnchorTestSupport.honestHeader(H + 41L, parent.getHash(), List.of());
-
- assertThat(FalconSealSupport.instance().observedValidatorsHeight()).isEqualTo(-1L);
- assertThat(rule.validate(block, parent, contextWith(VALIDATORS)))
- .describedAs("K is 0 at this height, so an empty certificate is legitimate")
- .isTrue();
- assertThat(FalconSealSupport.instance().observedValidatorsHeight())
- .describedAs(
- "the rule that takes over at H must also take over feeding the coverage report, or the "
- + "report is about a height the chain left behind")
- .isEqualTo(parent.getNumber());
- }
-
- @Test
- public void belowTheAnchorHeightTheSealsRuleRecordsNothing() {
- // The negative control for the test above: a rule that recorded unconditionally would pass it
- // while breaking the height gate that keeps the whole scheme inert below H.
- final PqAnchorSealsRule rule = new PqAnchorSealsRule(ARMED, new NoRegistry());
- final BlockHeader parent = parentHeader(H - 3L);
- final BlockHeader block = PqAnchorTestSupport.honestHeader(H - 2L, parent.getHash(), List.of());
-
- assertThat(rule.validate(block, parent, contextWith(VALIDATORS))).isTrue();
- assertThat(FalconSealSupport.instance().observedValidatorsHeight())
- .describedAs("below H this rule does nothing at all, recording included")
- .isEqualTo(-1L);
- }
-
- private static ProtocolContext contextWith(final Collection validators) {
- final ValidatorProvider validatorProvider =
- mock(ValidatorProvider.class, withSettings().strictness(Strictness.LENIENT));
- when(validatorProvider.getValidatorsForBlock(any())).thenReturn(validators);
- when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(validators);
- final BftContext bftContext =
- mock(BftContext.class, withSettings().strictness(Strictness.LENIENT));
- when(bftContext.getValidatorProvider()).thenReturn(validatorProvider);
- when(bftContext.as(any())).thenReturn(bftContext);
- return new ProtocolContext.Builder().withConsensusContext(bftContext).build();
- }
-
- private static void forgetFalconSingleton() throws Exception {
- final Field f = FalconSealSupport.class.getDeclaredField("instance");
- f.setAccessible(true);
- f.set(null, null);
- }
-
- /** A registry that binds nothing: this file measures the feed, never the verification. */
- private static final class NoRegistry
- implements org.hyperledger.besu.consensus.common.bft.PqSignerRegistry {
-
- // D2 (2026-08-06): the height-less pair was deleted from PqSignerRegistry, so this double now
- // has to answer "at which height" like everything else. It still binds nothing.
- @Override
- public Address addressForIndexAtOwnHead(final long blockNumber, final int validatorIndex) {
- return addressForIndexAtHistoric(blockNumber, validatorIndex);
- }
-
- @Override
- public boolean verifyAtOwnHead(
- final long blockNumber,
- final int validatorIndex,
- final org.apache.tuweni.bytes.Bytes message,
- final org.apache.tuweni.bytes.Bytes signature) {
- return verifyAtHistoric(blockNumber, validatorIndex, message, signature);
- }
-
- @Override
- public Address addressForIndexAtHistoric(final long blockNumber, final int validatorIndex) {
- return null;
- }
-
- @Override
- public boolean verifyAtHistoric(
- final long blockNumber,
- final int validatorIndex,
- final org.apache.tuweni.bytes.Bytes message,
- final org.apache.tuweni.bytes.Bytes signature) {
- return false;
- }
-
- @Override
- public String toString() {
- return "NoRegistry";
- }
- }
-}
diff --git a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D079ArmedWithoutRegistryTest.java b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D079ArmedWithoutRegistryTest.java
deleted file mode 100644
index f2b8dcf..0000000
--- a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/D079ArmedWithoutRegistryTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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.consensus.qbft.headervalidationrules;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.VALIDATORS;
-import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.parentHeader;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.mockito.Mockito.withSettings;
-
-import org.hyperledger.besu.consensus.common.bft.BftContext;
-import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
-import org.hyperledger.besu.consensus.common.bft.PqV2Fixture;
-import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
-import org.hyperledger.besu.datatypes.Address;
-import org.hyperledger.besu.ethereum.ProtocolContext;
-import org.hyperledger.besu.ethereum.core.BlockHeader;
-
-import java.lang.reflect.Field;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.Collection;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-import org.mockito.quality.Strictness;
-
-/**
- * D-079, THE RESIDUAL: the accident the configuration guard cannot refuse.
- *
- *