diff --git a/IMPORT-PROOF-STARE-2026-08-15.md b/IMPORT-PROOF-STARE-2026-08-15.md
index 3a0e878..b4e4bb3 100644
--- a/IMPORT-PROOF-STARE-2026-08-15.md
+++ b/IMPORT-PROOF-STARE-2026-08-15.md
@@ -101,3 +101,42 @@ istorice de ancorare dezarmata**, numite cu margini exacte, in care regulile de
antetul asa cum e, exact ca ferestrele de exceptie ale pragului de taxa pe care al doilea client le
poarta deja. Reparatia e in lucru; pana e dovedita prin import dincolo de fereastra, aceasta
sectiune spune limita in loc sa o ascunda.
+
+
+## DOVADA COMPLETA: nodul din pachete a ajuns la varf, 16 august
+
+**Un nod construit numai din pachetele publice a importat lantul 2800 de la blocul 0 pana la
+varful viu, si acum il urmareste.** Ultima inaltime atinsa in aceasta rulare: **14.088.211**, si nodul raporta ca nu mai sincronizeaza, adica prinsese varful. **Zero respingeri de ancora, zero esecuri de import, zero nepotriviri de
+stare.** A trecut de fereastra dezarmata, de inaltimea consensului blocant complet, si a prins
+varful.
+
+Ca sa ajunga acolo au trebuit doua lucruri, si al doilea nu era in ipoteza initiala.
+
+**Intai, fereastra in care ancorarea a fost dezarmata**, descrisa mai sus: 36 de inaltimi de
+ancora, 13.267.824 pana la 13.268.944, in care blocurile poarta certificat atasat dar nu poarta
+digest, si in care indicii semnatarilor nu cresc strict. Rezolvata cu o lista de ferestre
+istorice numite, cu margini fixe in cod, dupa modelul ferestrelor de exceptie ale pragului de
+taxa: inauntru antetul se accepta asa cum e, in afara nu se schimba absolut nimic.
+
+**Apoi, recuperarea de dupa incident**, gasita masurand, nu ghicind. Dupa rearmarea de urgenta
+flota a mers o vreme cu pragul de sigilii coborat, deci propunatorii au scris legitim certificate
+mai scurte. Intinderea ei s-a masurat citind **25.252 de inaltimi de ancora una cate una**, fara
+cautare binara, fiindca proprietatea nu e monotona: doar ~15 la suta din inaltimi sunt atinse si
+cea mai mare distanta intre doua atinse e de 44 de inaltimi, deci o cautare binara ar fi dat un
+raspuns care arata exact ca unul bun. Rezultatul: **13.268.976 pana la 13.890.544**, 19.425 de
+inaltimi, cu praguri masurate de 1 sau 2, niciodata 0.
+
+**Relaxarea celei de-a doua ferestre atinge NUMAI numarul de sigilii.** Legarea digestului si
+ordonarea indicilor raman impuse, si fiecare sigiliu e in continuare verificat. Alternativa
+lenesa, largirea ferestrei dezarmate ca sa le acopere pe amandoua, ar fi aruncat legarea
+certificatului pe 19.425 de inaltimi de ancora. Aceea ar fi fost o reparatie care sterge exact
+proprietatea pentru care exista ancora.
+
+**Controlul negativ, in patru directii, fiindca o exceptie poate gresi in ambele sensuri:**
+predicatul de fereastra fortat mereu fals inroseste 10 probe (ce e inauntru nu mai e acceptat);
+fortat mereu adevarat inroseste 39, dintre care 22 sunt probe de strictete care existau dinainte,
+adica se vede negru pe alb exceptia inghitind lantul; pragul istoric golit inroseste 7; pragul
+istoric fixat la 1 inroseste 8. Repus, totul verde: **641 de probe, 0 esecuri**, numarate din XML.
+
+Peticul publicat poarta amandoua ferestrele si a fost verificat pe un checkout pristin al
+amontelui, singur si in serie, iar arborele rezultat compileaza.
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
index a383bcf..59c2d86 100644
--- a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
@@ -249,11 +249,17 @@ public final class PqAnchor {
* [13,266,800, 13,269,400] straight off the public endpoint: 82 anchor headers, 46 sorted, 36 not,
* none without a certificate. The 36 are CONTIGUOUS at the 32-block anchor spacing and nothing
* outside the window is affected, so the range below is the measured extent, not a guess.
+ *
+ *
DERIVED, not written a second time. The bounds themselves live in {@link PqAnchorLapse},
+ * which is the one place a historical window is named, because the SAME interruption that left
+ * these certificates unsorted also left their headers without an anchor digest, and a value that
+ * appears in two places diverges. This constant is kept because it is the name this class has
+ * always used for the first bound.
*/
- public static final long UNORDERED_WINDOW_FIRST = 13_267_824L;
+ public static final long UNORDERED_WINDOW_FIRST = PqAnchorLapse.windows().get(0).firstBlock();
/** Last anchor height of that window. Its neighbours 13,267,792 and 13,268,976 are both sorted. */
- public static final long UNORDERED_WINDOW_LAST = 13_268_944L;
+ public static final long UNORDERED_WINDOW_LAST = PqAnchorLapse.windows().get(0).lastBlock();
/**
* Whether the certificate's indices are non-negative and pairwise DISTINCT, in any order.
@@ -315,9 +321,7 @@ public final class PqAnchor {
if (hasStrictlyIncreasingIndices(certificate)) {
return true;
}
- final boolean inHistoricalWindow =
- blockNumber >= UNORDERED_WINDOW_FIRST && blockNumber <= UNORDERED_WINDOW_LAST;
- return inHistoricalWindow && hasDistinctNonNegativeIndices(certificate);
+ return PqAnchorLapse.isDisarmed(blockNumber) && hasDistinctNonNegativeIndices(certificate);
}
/**
diff --git a/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java
new file mode 100644
index 0000000..a7c8c9b
--- /dev/null
+++ b/anchor/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java
@@ -0,0 +1,375 @@
+/*
+ * 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 java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.OptionalInt;
+
+/**
+ * The historical ranges on chain 2800 in which the anchor rules were not fully in force.
+ *
+ *
WHAT HAPPENED. On 2026-08-10 all nine validators were restarted inside a few minutes. A
+ * node that has just restarted holds no Falcon seals until it takes part in one commit, so for a
+ * moment every node held none; the very next height was an anchor height, its threshold asked for
+ * three seals, nobody could propose, and the chain stopped for nine minutes. The measure that
+ * restarted it was to switch the anchor rules off on all nine nodes at once, and then to restore
+ * them with the emergency seal ceiling lowered rather than at the scheduled threshold. That leaves
+ * TWO ranges of canonical headers, not one, and they fail a strict validator for different reasons:
+ *
+ *
+ * - The rules off entirely. vanityData carries the client's usual version string instead
+ * of the anchor digest, although the certificate is attached, so the digest binding does not
+ * hold; and the certificate's validator indices are in the order the commit messages ARRIVED
+ * rather than sorted, so they are distinct but not increasing.
+ *
- The rules on, the threshold lowered. vanityData carries a correct digest and the
+ * indices are sorted, so both of those bind exactly as they should. What differs is the COUNT:
+ * a proposer running under the lowered ceiling legitimately wrote fewer seals than the
+ * configured schedule asks for, so the header is short of the threshold and nothing else.
+ *
+ *
+ * WHY THE EXCEPTION BELONGS AT VALIDATION AND NOWHERE ELSE. Nodes that already hold these
+ * blocks never revalidate them, which is why the damage was invisible. A node built from this source
+ * and synced from genesis does validate them, and under the plain rules it stops at the first header
+ * of each range and never passes it. The history is what it is: no recomputation can turn these
+ * headers into headers that satisfy a rule they were written without. What a correct client can do
+ * is name the ranges and judge those headers by the rule that was actually in force when they were
+ * written, which is what every node holding the chain does today. A patch anywhere else moves the
+ * error instead of ending it.
+ *
+ *
THE BOUNDS, MEASURED, NOT SEARCHED. Neither property is monotone: inside the second
+ * range only about one anchor header in seven is short of the threshold and the rest are not, so a
+ * binary search would return a height that looks exactly like an answer and is not one. Every anchor
+ * height from the activation height 13,014,000 to the head at the time of measurement, 14,077,000,
+ * was therefore read one by one and its vanityData and certificate decoded: 25,252 heights,
+ * zero transport errors, and exactly the two contiguous ranges named below. Outside them there is
+ * not one header without a digest, not one with unsorted indices, and not one below the threshold.
+ *
+ *
WHAT THE RANGES DO NOT RELAX, AT ANY HEIGHT. Over the first range a certificate must
+ * still decode and must still carry non-negative, pairwise DISTINCT indices: sortedness fixes one
+ * accepted order for a given set, while distinctness is the part that is load bearing against an
+ * attacker, because it is what makes "repeat one seal to inflate the count" unrepresentable in the
+ * grammar of the format. Over the second range NOTHING is relaxed except the count, and even the
+ * count keeps a floor: the lowest number of seals measured anywhere in that range is
+ * 1, which is the ceiling that was actually in force, so that is the floor applied,
+ * not zero. Both ranges therefore admit exactly the history that exists and nothing weaker.
+ *
+ *
THE BOUNDS ARE FIXED IN CODE ON PURPOSE. A range a node could widen at runtime would let
+ * a future lapse pass unnoticed. Outside these ranges nothing changes at all, so a NEW unenforced
+ * header still stops this client, which is what should happen. Producers never consult this class:
+ * new headers are always written with the digest, with sorted indices and at the scheduled
+ * threshold, so the ranges are closed by construction and cannot grow.
+ *
+ *
THE TYPE IS A LIST BECAUSE A THIRD RANGE MUST NOT REQUIRE TOUCHING A RULE. The two
+ * entries below are the two the chain has. Adding another is an edit to this file alone.
+ */
+public final class PqAnchorLapse {
+
+ /** The chain these ranges belong to. They describe no other chain. */
+ public static final long CHAIN_ID = 2800L;
+
+ /** The anchor spacing in force over both ranges, used by the self-check below. */
+ public static final long ANCHOR_SPACING = 32L;
+
+ /** What was not in force over a range. */
+ public enum Relaxation {
+ /** Nothing was in force: no digest binding, no ordering, no threshold. */
+ EVERYTHING,
+ /** The digest and the ordering were in force; only the seal threshold was lowered. */
+ THRESHOLD_ONLY
+ }
+
+ /**
+ * One contiguous range of heights over which the anchor rules were not fully in force.
+ *
+ *
Both bounds are INCLUSIVE and both are measured anchor heights: the first is the first header
+ * that shows the defect and the last is the last one that shows it.
+ */
+ public static final class Window {
+
+ private final long firstBlock;
+ private final long lastBlock;
+ private final long anchorHeights;
+ private final Relaxation relaxation;
+ private final int effectiveMinSeals;
+ private final String reason;
+
+ /**
+ * Names a range.
+ *
+ * @param firstBlock the first affected height, inclusive
+ * @param lastBlock the last affected height, inclusive
+ * @param anchorHeights how many anchor heights the range spans; carried so that the count and
+ * the bounds are checked against each other rather than written twice
+ * @param relaxation what was not in force
+ * @param effectiveMinSeals the seal threshold that WAS in force over the range, which is the
+ * floor still applied inside it; zero when nothing was in force
+ * @param reason why the range exists, and what was measured
+ */
+ public Window(
+ final long firstBlock,
+ final long lastBlock,
+ final long anchorHeights,
+ final Relaxation relaxation,
+ final int effectiveMinSeals,
+ final String reason) {
+ if (firstBlock < 0L || lastBlock < firstBlock) {
+ throw new IllegalArgumentException(
+ "AERE PQ ANCHOR LAPSE: a window must be a non-negative, non-empty range, got ["
+ + firstBlock
+ + ", "
+ + lastBlock
+ + "]");
+ }
+ if (anchorHeights < 1L) {
+ throw new IllegalArgumentException(
+ "AERE PQ ANCHOR LAPSE: a window must span at least one anchor height, got "
+ + anchorHeights);
+ }
+ if (effectiveMinSeals < 0) {
+ throw new IllegalArgumentException(
+ "AERE PQ ANCHOR LAPSE: the threshold in force cannot be negative, got "
+ + effectiveMinSeals);
+ }
+ this.firstBlock = firstBlock;
+ this.lastBlock = lastBlock;
+ this.anchorHeights = anchorHeights;
+ this.relaxation = relaxation;
+ this.effectiveMinSeals = effectiveMinSeals;
+ this.reason = reason;
+ }
+
+ /**
+ * The first affected height.
+ *
+ * @return the inclusive lower bound
+ */
+ public long firstBlock() {
+ return firstBlock;
+ }
+
+ /**
+ * The last affected height.
+ *
+ * @return the inclusive upper bound
+ */
+ public long lastBlock() {
+ return lastBlock;
+ }
+
+ /**
+ * How many anchor heights this range spans.
+ *
+ * @return the span, in anchor heights
+ */
+ public long anchorHeights() {
+ return anchorHeights;
+ }
+
+ /**
+ * What was not in force over this range.
+ *
+ * @return the relaxation
+ */
+ public Relaxation relaxation() {
+ return relaxation;
+ }
+
+ /**
+ * The seal threshold that WAS in force over this range.
+ *
+ * @return the measured floor, zero when no rule was in force
+ */
+ public int effectiveMinSeals() {
+ return effectiveMinSeals;
+ }
+
+ /**
+ * Why this range exists.
+ *
+ * @return the reason, with what was measured
+ */
+ public String reason() {
+ return reason;
+ }
+
+ /**
+ * Whether a height falls inside this range.
+ *
+ * @param blockNumber the height being judged
+ * @return true iff the height is within the inclusive bounds
+ */
+ public boolean covers(final long blockNumber) {
+ return blockNumber >= firstBlock && blockNumber <= lastBlock;
+ }
+
+ @Override
+ public String toString() {
+ return "["
+ + firstBlock
+ + ", "
+ + lastBlock
+ + "] "
+ + relaxation
+ + " ("
+ + anchorHeights
+ + " anchor heights, threshold in force "
+ + effectiveMinSeals
+ + ")";
+ }
+ }
+
+ private static final List WINDOWS =
+ List.of(
+ new Window(
+ 13_267_824L,
+ 13_268_944L,
+ 36L,
+ Relaxation.EVERYTHING,
+ 0,
+ "2026-08-10: the anchor rules were switched off on the whole validator set to restart "
+ + "a stopped chain, so the headers produced meanwhile carry the client version "
+ + "string in vanityData instead of the anchor digest, and their certificates "
+ + "carry the indices in arrival order rather than sorted. Measured: 36 anchor "
+ + "heights, all 36 affected, all with distinct non-negative indices"),
+ new Window(
+ 13_268_976L,
+ 13_890_544L,
+ 19_425L,
+ Relaxation.THRESHOLD_ONLY,
+ 1,
+ "2026-08-10 onwards: the rules were restored immediately afterwards "
+ + "but with the emergency seal ceiling lowered, because raising the threshold "
+ + "back in one step had stopped the chain a second time. Every header here binds "
+ + "its digest and carries sorted indices; 2,926 of the 19,425 "
+ + "anchor heights in the range simply carry fewer seals than the schedule asks, "
+ + "the fewest being 1"));
+
+ static {
+ // The list is ordered and disjoint, and each entry's stated span agrees with its own bounds.
+ // This runs at class initialisation so that a future entry which contradicts itself, or which
+ // overlaps its neighbour, fails loudly at startup rather than silently widening what a node
+ // accepts.
+ Window previous = null;
+ for (final Window window : WINDOWS) {
+ if (previous != null && window.firstBlock() <= previous.lastBlock()) {
+ throw new IllegalStateException(
+ "AERE PQ ANCHOR LAPSE: windows must be ordered and disjoint, "
+ + previous
+ + " overlaps "
+ + window);
+ }
+ final long derived = (window.lastBlock() - window.firstBlock()) / ANCHOR_SPACING + 1L;
+ if (derived != window.anchorHeights()) {
+ throw new IllegalStateException(
+ "AERE PQ ANCHOR LAPSE: window "
+ + window
+ + " says it spans "
+ + window.anchorHeights()
+ + " anchor heights, but its bounds at spacing "
+ + ANCHOR_SPACING
+ + " span "
+ + derived);
+ }
+ if (window.relaxation() == Relaxation.EVERYTHING && window.effectiveMinSeals() != 0) {
+ throw new IllegalStateException(
+ "AERE PQ ANCHOR LAPSE: window " + window + " has no rule in force, so no threshold");
+ }
+ if (window.relaxation() == Relaxation.THRESHOLD_ONLY && window.effectiveMinSeals() < 1) {
+ throw new IllegalStateException(
+ "AERE PQ ANCHOR LAPSE: window "
+ + window
+ + " relaxes only the threshold, so the threshold it replaces it with must be at "
+ + "least 1; a floor of zero would accept an empty certificate where a certificate "
+ + "was in fact required");
+ }
+ previous = window;
+ }
+ }
+
+ private PqAnchorLapse() {}
+
+ /**
+ * Every named range, ordered by first height.
+ *
+ * @return an unmodifiable list, never null, possibly empty
+ */
+ public static List windows() {
+ return WINDOWS;
+ }
+
+ /**
+ * Whether the anchor rules were out of force ENTIRELY at this height.
+ *
+ * This is the single question both anchor rules ask before they judge anything. They ask it
+ * through this one method, for the same reason they share {@code anchorAppliesAt}: two rules that
+ * each decided the boundary their own way could disagree about one header, and a header accepted
+ * by one rule and refused by the other is a chain break.
+ *
+ *
It is deliberately FALSE inside a threshold-only range. There the digest binding and the
+ * ordering did hold and are still required; only {@link #historicMinSeals(long)} moves.
+ *
+ * @param blockNumber the height being judged
+ * @return true iff the height falls inside a range where nothing was in force
+ */
+ public static boolean isDisarmed(final long blockNumber) {
+ for (final Window window : WINDOWS) {
+ if (window.covers(blockNumber) && window.relaxation() == Relaxation.EVERYTHING) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * The seal threshold that was actually in force at this height, when it was lower than the
+ * configured schedule.
+ *
+ *
This is the same lever the fleet itself used at the time, the emergency ceiling, replayed
+ * from a fixed range instead of from a runtime option. A node validating history therefore asks
+ * the certificate for what was asked of the proposer that wrote it, and nothing more.
+ *
+ * @param blockNumber the height being judged
+ * @return the threshold in force, or empty when the configured schedule applies unchanged
+ */
+ public static OptionalInt historicMinSeals(final long blockNumber) {
+ for (final Window window : WINDOWS) {
+ if (window.covers(blockNumber) && window.relaxation() == Relaxation.THRESHOLD_ONLY) {
+ return OptionalInt.of(window.effectiveMinSeals());
+ }
+ }
+ return OptionalInt.empty();
+ }
+
+ /**
+ * The range covering a height, when there is one.
+ *
+ * @param blockNumber the height being judged
+ * @return the range, or empty
+ */
+ public static Optional windowCovering(final long blockNumber) {
+ return WINDOWS.stream().filter(w -> w.covers(blockNumber)).findFirst();
+ }
+
+ /**
+ * The first height of the earliest named range, or empty when there is none.
+ *
+ * @return the earliest affected height
+ */
+ public static Optional earliestAffectedBlock() {
+ return WINDOWS.stream().map(Window::firstBlock).min(Comparator.naturalOrder());
+ }
+}
diff --git a/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java
new file mode 100644
index 0000000..038d862
--- /dev/null
+++ b/anchor/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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.util.List;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * The named historical ranges in which the anchor rules were not fully in force.
+ *
+ * The bounds are the whole content of the class under test, so they are asserted as LITERALS
+ * rather than through the constants they define. An assertion written as {@code
+ * assertThat(FIRST).isEqualTo(FIRST)} would follow any future edit of the constant and would
+ * therefore never fail, which is the exact shape of a gate that cannot go red.
+ */
+public class PqAnchorLapseTest {
+
+ private static final long LAST_ENFORCED_BEFORE = 13_267_792L;
+
+ private static final long DISARMED_FIRST = 13_267_824L;
+ private static final long DISARMED_LAST = 13_268_944L;
+
+ private static final long LOWERED_FIRST = 13_268_976L;
+ private static final long LOWERED_LAST = 13_890_544L;
+
+ private static final long FIRST_FULLY_ENFORCED_AFTER = 13_890_576L;
+
+ @Test
+ public void theListNamesExactlyTheTwoMeasuredRanges() {
+ // Measured by reading vanityData and the certificate at every anchor height from the activation
+ // height 13,014,000 to the head at the time of measurement, 14,077,000, one height at a time.
+ // The list is a LIST so a third could be added without touching a rule; it has two because the
+ // chain has two.
+ assertThat(PqAnchorLapse.windows()).hasSize(2);
+
+ final PqAnchorLapse.Window disarmed = PqAnchorLapse.windows().get(0);
+ assertThat(disarmed.firstBlock()).isEqualTo(13_267_824L);
+ assertThat(disarmed.lastBlock()).isEqualTo(13_268_944L);
+ assertThat(disarmed.anchorHeights()).isEqualTo(36L);
+ assertThat(disarmed.relaxation()).isEqualTo(PqAnchorLapse.Relaxation.EVERYTHING);
+ assertThat(disarmed.effectiveMinSeals()).isZero();
+ assertThat(disarmed.reason()).contains("2026-08-10");
+
+ final PqAnchorLapse.Window lowered = PqAnchorLapse.windows().get(1);
+ assertThat(lowered.firstBlock()).isEqualTo(13_268_976L);
+ assertThat(lowered.lastBlock()).isEqualTo(13_890_544L);
+ assertThat(lowered.anchorHeights()).isEqualTo(19_425L);
+ assertThat(lowered.relaxation()).isEqualTo(PqAnchorLapse.Relaxation.THRESHOLD_ONLY);
+ assertThat(lowered.effectiveMinSeals()).isEqualTo(1);
+ }
+
+ @Test
+ public void theTwoRangesAreAdjacentButDisjoint() {
+ // The second begins at the next anchor height after the first ends. They must not overlap, and
+ // there must be no unnamed anchor height between them, because a height in neither range is
+ // judged strictly and there is no such height here.
+ assertThat(DISARMED_LAST + PqAnchorLapse.ANCHOR_SPACING).isEqualTo(LOWERED_FIRST);
+ assertThat(PqAnchorLapse.windows().get(0).lastBlock())
+ .isLessThan(PqAnchorLapse.windows().get(1).firstBlock());
+ }
+
+ @Test
+ public void theStatedSpansAgreeWithTheBounds() {
+ // 36, not 37: (13,268,944 - 13,267,824) / 32 + 1 = 36. And 19,425 for the second range. The
+ // span is carried in each entry so the two cannot drift apart in silence; this asserts the
+ // arithmetic from the outside as well.
+ for (final PqAnchorLapse.Window window : PqAnchorLapse.windows()) {
+ assertThat((window.lastBlock() - window.firstBlock()) % PqAnchorLapse.ANCHOR_SPACING).isZero();
+ assertThat((window.lastBlock() - window.firstBlock()) / PqAnchorLapse.ANCHOR_SPACING + 1L)
+ .isEqualTo(window.anchorHeights());
+ }
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // isDisarmed: TRUE only in the first range, and deliberately FALSE in the second.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void isDisarmedIsTrueOnlyWhereNothingWasInForce() {
+ assertThat(PqAnchorLapse.isDisarmed(DISARMED_FIRST)).isTrue();
+ assertThat(PqAnchorLapse.isDisarmed(DISARMED_LAST)).isTrue();
+
+ // The neighbouring anchor heights, both measured to carry a digest, stay under the strict rules.
+ assertThat(PqAnchorLapse.isDisarmed(LAST_ENFORCED_BEFORE)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(DISARMED_FIRST - 1L)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(DISARMED_LAST + 1L)).isFalse();
+
+ // And FALSE across the whole second range: there the digest and the ordering did hold, and a
+ // relaxation that leaked into it would throw away the binding on 19,425 anchor heights.
+ assertThat(PqAnchorLapse.isDisarmed(LOWERED_FIRST)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(13_500_016L)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(LOWERED_LAST)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(FIRST_FULLY_ENFORCED_AFTER)).isFalse();
+ }
+
+ @Test
+ public void everyAnchorHeightOfTheDisarmedRangeIsCovered() {
+ long covered = 0L;
+ for (long height = DISARMED_FIRST;
+ height <= DISARMED_LAST;
+ height += PqAnchorLapse.ANCHOR_SPACING) {
+ assertThat(PqAnchorLapse.isDisarmed(height))
+ .describedAs("anchor height %d of the disarmed range", height)
+ .isTrue();
+ covered++;
+ }
+ assertThat(covered).isEqualTo(36L);
+
+ // The rest of the chain is untouched.
+ assertThat(PqAnchorLapse.isDisarmed(0L)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(13_014_000L)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(13_000_000L)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(14_077_000L)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(Long.MAX_VALUE)).isFalse();
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // historicMinSeals: the threshold that was really in force, and only where it was lower.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void historicMinSealsAnswersOnlyInsideTheLoweredRange() {
+ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST)).hasValue(1);
+ assertThat(PqAnchorLapse.historicMinSeals(13_500_016L)).hasValue(1);
+ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_LAST)).hasValue(1);
+
+ // One anchor step past the end and the configured schedule applies again, unchanged. This is
+ // the assertion that stops the lowered threshold from becoming permanent.
+ assertThat(PqAnchorLapse.historicMinSeals(FIRST_FULLY_ENFORCED_AFTER)).isEmpty();
+ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_LAST + 1L)).isEmpty();
+ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST - 1L)).isEmpty();
+ assertThat(PqAnchorLapse.historicMinSeals(LAST_ENFORCED_BEFORE)).isEmpty();
+ assertThat(PqAnchorLapse.historicMinSeals(14_077_000L)).isEmpty();
+ assertThat(PqAnchorLapse.historicMinSeals(Long.MAX_VALUE)).isEmpty();
+
+ // And it does NOT answer inside the disarmed range: there the rules are skipped whole, so a
+ // threshold would be a second, contradictory way of saying the same thing.
+ assertThat(PqAnchorLapse.historicMinSeals(DISARMED_FIRST)).isEmpty();
+ assertThat(PqAnchorLapse.historicMinSeals(DISARMED_LAST)).isEmpty();
+ }
+
+ @Test
+ public void theFloorInsideTheLoweredRangeIsOneAndNotZero() {
+ // Measured: 2,926 of the 19,425 anchor heights in that range carry fewer seals than the
+ // schedule asks, the counts seen are 1 and 2, and NOT ONE carries zero. A floor of zero would
+ // therefore accept an empty certificate at a height where a certificate was in fact required,
+ // which is weaker than the history needs.
+ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST)).hasValue(1);
+ assertThat(PqAnchorLapse.windows().get(1).effectiveMinSeals()).isPositive();
+ }
+
+ @Test
+ public void windowCoveringNamesTheRangeOrNothing() {
+ assertThat(PqAnchorLapse.windowCovering(DISARMED_FIRST)).isPresent();
+ assertThat(PqAnchorLapse.windowCovering(DISARMED_FIRST).get().lastBlock())
+ .isEqualTo(DISARMED_LAST);
+ assertThat(PqAnchorLapse.windowCovering(LOWERED_FIRST)).isPresent();
+ assertThat(PqAnchorLapse.windowCovering(LOWERED_FIRST).get().relaxation())
+ .isEqualTo(PqAnchorLapse.Relaxation.THRESHOLD_ONLY);
+ assertThat(PqAnchorLapse.windowCovering(FIRST_FULLY_ENFORCED_AFTER)).isEmpty();
+ assertThat(PqAnchorLapse.earliestAffectedBlock()).contains(DISARMED_FIRST);
+ }
+
+ @Test
+ public void theDisarmedRangeIsTheSameOBJECTTheOrderingExceptionUses() {
+ // A second copy of these bounds anywhere else is a value that diverges. PqAnchor used to carry
+ // them as its own literals; it now derives them, and this is the assertion that says so.
+ assertThat(PqAnchor.UNORDERED_WINDOW_FIRST).isEqualTo(DISARMED_FIRST);
+ assertThat(PqAnchor.UNORDERED_WINDOW_LAST).isEqualTo(DISARMED_LAST);
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // The self-checks can say no. A guard that has never refused anything cannot be believed.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void aRangeThatContradictsItselfIsRefusedRatherThanAccepted() {
+ assertThatThrownBy(
+ () ->
+ new PqAnchorLapse.Window(
+ 100L, 99L, 1L, PqAnchorLapse.Relaxation.EVERYTHING, 0, "backwards"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("non-empty range");
+ assertThatThrownBy(
+ () ->
+ new PqAnchorLapse.Window(
+ -1L, 100L, 1L, PqAnchorLapse.Relaxation.EVERYTHING, 0, "negative"))
+ .isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(
+ () ->
+ new PqAnchorLapse.Window(
+ 100L, 200L, 0L, PqAnchorLapse.Relaxation.EVERYTHING, 0, "no heights"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("at least one anchor height");
+ assertThatThrownBy(
+ () ->
+ new PqAnchorLapse.Window(
+ 100L, 200L, 4L, PqAnchorLapse.Relaxation.THRESHOLD_ONLY, -1, "negative floor"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("cannot be negative");
+ }
+
+ @Test
+ public void aRangeCoversItsOwnBoundsAndNothingOutsideThem() {
+ final PqAnchorLapse.Window window =
+ new PqAnchorLapse.Window(
+ 1_000L, 1_032L, 2L, PqAnchorLapse.Relaxation.THRESHOLD_ONLY, 1, "probe");
+ assertThat(window.covers(1_000L)).isTrue();
+ assertThat(window.covers(1_016L)).isTrue();
+ assertThat(window.covers(1_032L)).isTrue();
+ assertThat(window.covers(999L)).isFalse();
+ assertThat(window.covers(1_033L)).isFalse();
+ assertThat(window.toString()).contains("1000").contains("1032").contains("THRESHOLD_ONLY");
+ }
+
+ @Test
+ public void theListIsUnmodifiable() {
+ // A range a node could widen at runtime would let a future lapse pass unnoticed.
+ final List windows = PqAnchorLapse.windows();
+ assertThatThrownBy(
+ () ->
+ windows.add(
+ new PqAnchorLapse.Window(
+ 1L, 2L, 1L, PqAnchorLapse.Relaxation.EVERYTHING, 0, "injected")))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ @Test
+ public void theRangesBelongToThisChainAndAreDeclaredSo() {
+ assertThat(PqAnchorLapse.CHAIN_ID).isEqualTo(2800L);
+ assertThat(PqAnchorLapse.ANCHOR_SPACING).isEqualTo(32L);
+ }
+}
diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java
index c79a884..ce64645 100644
--- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java
+++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java
@@ -19,6 +19,7 @@ import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec;
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
+import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.mainnet.DetachedBlockHeaderValidationRule;
@@ -128,6 +129,16 @@ public class PqAnchorDigestRule implements DetachedBlockHeaderValidationRule {
return true;
}
+ // SECOND GATE: the named historical windows in which these rules were not in force. Inside one
+ // of them vanityData carries the client's version string rather than a digest, so there is no
+ // digest to match and no recomputation can produce one; refusing would stop this node forever
+ // at a height the whole network holds. See PqAnchorLapse for what happened, how the bounds were
+ // measured, and why an exception written anywhere other than validation moves the error instead
+ // of ending it.
+ if (PqAnchorLapse.isDisarmed(number)) {
+ return acceptAsWrittenInsideLapse(header, number);
+ }
+
try {
final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
@@ -193,6 +204,53 @@ public class PqAnchorDigestRule implements DetachedBlockHeaderValidationRule {
}
}
+ /**
+ * Accept a header from a named unenforced window as it was written, after the one check that is
+ * not relaxed at any height.
+ *
+ * Distinctness is kept and the difference is the whole reason this is not a bare {@code return
+ * true}: sortedness fixes ONE accepted order for a given set, while distinctness is what makes
+ * "repeat one seal to inflate the count" unrepresentable in the grammar of the format. Every
+ * header of the measured window has distinct, non-negative indices, so this admits exactly the
+ * history that exists and nothing weaker.
+ *
+ *
Decoding must still succeed. A header this rule cannot parse has not been shown to be one of
+ * the headers the window is about, so it is refused here as everywhere else in this file.
+ *
+ * @param header the header being judged
+ * @param number its height
+ * @return true iff the carried certificate decodes and its indices are distinct and non-negative
+ */
+ private boolean acceptAsWrittenInsideLapse(final BlockHeader header, final long number) {
+ try {
+ final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
+ final List certificate = List.copyOf(extraData.getFalconSeals());
+ if (!PqAnchor.hasDistinctNonNegativeIndices(certificate)) {
+ LOG.warn(
+ "AERE PQ ANCHOR (R1): block {} REJECTED - it falls inside the historical range {} in "
+ + "which the anchor rules were not in force, but its certificate repeats a "
+ + "validator index or carries a negative one. That is not a shape any header of "
+ + "that range has, and it is the one property the exception does not relax.",
+ number,
+ PqAnchorLapse.windowCovering(number).orElse(null));
+ return false;
+ }
+ LOG.debug(
+ "AERE PQ ANCHOR (R1): block {} accepted as written; it falls inside the historical range "
+ + "{} in which the anchor rules were not in force, so it carries no digest to bind.",
+ number,
+ PqAnchorLapse.windowCovering(number).orElse(null));
+ return true;
+ } catch (final Exception e) {
+ LOG.warn(
+ "AERE PQ ANCHOR (R1): block {} REJECTED - it falls inside a historical unenforced range "
+ + "but its extraData could not be decoded: {}",
+ number,
+ e.toString());
+ return false;
+ }
+ }
+
@Override
public boolean includeInLightValidation() {
return true;
diff --git a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
index a435074..9be0c7b 100644
--- a/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
+++ b/anchor/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
@@ -20,6 +20,7 @@ import org.hyperledger.besu.consensus.common.bft.FalconSeal;
import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
+import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
import org.hyperledger.besu.datatypes.Address;
@@ -30,6 +31,7 @@ import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.OptionalInt;
import java.util.Set;
import org.apache.tuweni.bytes.Bytes32;
@@ -125,6 +127,15 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
return true;
}
+ // SECOND GATE, and the SAME method R1 asks, for the same reason both rules share
+ // anchorAppliesAt: if the two rules each decided this boundary their own way, a header accepted
+ // by one and refused by the other would be a chain break. Inside a named window the threshold
+ // was not in force when the header was written and the indices were written in arrival order,
+ // so neither can be required of it now.
+ if (PqAnchorLapse.isDisarmed(number)) {
+ return acceptAsWrittenInsideLapse(header, number);
+ }
+
try {
if (parent == null) {
LOG.warn(
@@ -162,7 +173,26 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
final List certificate = List.copyOf(extraData.getFalconSeals());
final int k = certificate.size();
- final int required = config.minSealsAt(number);
+
+ // THE THRESHOLD THIS HEIGHT WAS ACTUALLY JUDGED BY. Normally the configured schedule. Inside
+ // a named historical range it is the LOWER of the schedule and the emergency ceiling that was
+ // in force when these headers were written, which is the same lever the fleet itself used at
+ // the time, replayed from a fixed range instead of from a runtime option. Asking a header for
+ // more than was asked of the proposer that wrote it refuses history that no node can
+ // reproduce; asking for the ceiling rather than for nothing keeps the strongest claim the
+ // range supports, and the ceiling is a measured floor, never zero. See PqAnchorLapse.
+ int required = config.minSealsAt(number);
+ final OptionalInt inForce = PqAnchorLapse.historicMinSeals(number);
+ if (inForce.isPresent() && inForce.getAsInt() < required) {
+ LOG.debug(
+ "AERE PQ ANCHOR (R2): block {} sits in the historical range {}, so the threshold "
+ + "applied is the {} that was in force then, not the {} the schedule asks for.",
+ number,
+ PqAnchorLapse.windowCovering(number).orElse(null),
+ inForce.getAsInt(),
+ required);
+ required = inForce.getAsInt();
+ }
if (k < required) {
LOG.warn(
@@ -285,6 +315,50 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
}
}
+ /**
+ * Accept a header from a named unenforced window as it was written, after the one check that is
+ * not relaxed at any height.
+ *
+ * The threshold is not applied here and neither is sortedness, because neither was applied
+ * when these headers were written; applying them now would refuse history the whole network
+ * holds. Distinctness IS applied, for the reason set out in {@link PqAnchor#hasAcceptableIndices}
+ * and in {@link PqAnchorLapse}: it is the part that is load bearing against an attacker rather
+ * than against ambiguity, and it is measured true on every header of the window.
+ *
+ * @param header the header being judged
+ * @param number its height
+ * @return true iff the carried certificate decodes and its indices are distinct and non-negative
+ */
+ private boolean acceptAsWrittenInsideLapse(final BlockHeader header, final long number) {
+ try {
+ final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
+ final List certificate = List.copyOf(extraData.getFalconSeals());
+ if (!PqAnchor.hasDistinctNonNegativeIndices(certificate)) {
+ LOG.warn(
+ "AERE PQ ANCHOR (R2): block {} REJECTED - it falls inside the historical range {} in "
+ + "which the anchor rules were not in force, but its certificate repeats a "
+ + "validator index or carries a negative one, which no header of that range does.",
+ number,
+ PqAnchorLapse.windowCovering(number).orElse(null));
+ return false;
+ }
+ LOG.debug(
+ "AERE PQ ANCHOR (R2): block {} accepted as written with {} seal(s); it falls inside the "
+ + "historical range {} in which neither the threshold nor the ordering was in force.",
+ number,
+ certificate.size(),
+ PqAnchorLapse.windowCovering(number).orElse(null));
+ return true;
+ } catch (final Exception e) {
+ LOG.warn(
+ "AERE PQ ANCHOR (R2): block {} REJECTED - it falls inside a historical unenforced range "
+ + "but its extraData could not be decoded: {}",
+ number,
+ e.toString());
+ return false;
+ }
+ }
+
/**
* AERE REGISTRY-COVERAGE REPORT: hand the parent's validator set to {@link FalconSealSupport} so
* its coverage report is about the height the chain is actually at. Swallows everything: a rule
diff --git a/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
new file mode 100644
index 0000000..9451ec7
--- /dev/null
+++ b/anchor/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
@@ -0,0 +1,548 @@
+/*
+ * 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.VALIDATORS;
+import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.extraData;
+import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.header;
+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.FalconSeal;
+import org.hyperledger.besu.consensus.common.bft.PqAnchor;
+import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
+import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
+import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
+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.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.OptionalInt;
+
+import org.apache.tuweni.bytes.Bytes;
+import org.apache.tuweni.bytes.Bytes32;
+import org.junit.jupiter.api.Test;
+import org.mockito.quality.Strictness;
+
+/**
+ * The two historical ranges in which the anchor rules were not fully in force, as the two rules see
+ * them.
+ *
+ * The configuration here is the one the live chain runs, not the small synthetic one the other
+ * rule tests use, because the whole subject is a set of REAL heights: the activation height, the
+ * 32-block anchor spacing and the threshold schedule all have to be the production ones, or the
+ * heights under test are not anchor heights at all and every assertion below would pass on a rule
+ * that does nothing. {@link #everyHeightUnderTestIsAnAnchorHeightOfTheLiveConfiguration()} is the
+ * positive control for exactly that.
+ *
+ *
Every acceptance in this file is paired with the same header shape at a height where it must
+ * still be refused, and every refusal is paired with an honest header at that same height, which
+ * must still be accepted. Without the first pairing the tests would pass on a rule that accepts
+ * everything; without the second they would pass on a rule that refuses everything.
+ */
+public class PqAnchorDisarmedWindowTest {
+
+ /** The live activation height H. */
+ private static final long H_LIVE = 13_014_000L;
+
+ /** The live anchor spacing. */
+ private static final int INTERVAL = 32;
+
+ /** The last anchor height fully under the rules before the interruption. */
+ private static final long BEFORE = 13_267_792L;
+
+ /** First height of the range in which nothing was in force. */
+ private static final long OFF_FIRST = 13_267_824L;
+
+ /** A height in the middle of that range. */
+ private static final long OFF_MIDDLE = 13_268_016L;
+
+ /** Last height of that range. */
+ private static final long OFF_LAST = 13_268_944L;
+
+ /** First height of the range in which only the threshold was lowered. */
+ private static final long LOW_FIRST = 13_268_976L;
+
+ /** A height in the middle of that range. */
+ private static final long LOW_MIDDLE = 13_500_016L;
+
+ /** Last height of that range. */
+ private static final long LOW_LAST = 13_890_544L;
+
+ /** The first anchor height fully under the rules again. */
+ private static final long AFTER = 13_890_576L;
+
+ /**
+ * The vanityData the headers of the first range actually carry: the client's version string,
+ * which is what a node writes when the anchor is switched off. Read off a live header of the
+ * range.
+ */
+ private static final Bytes CLIENT_VANITY =
+ Bytes.fromHexString("0x00000000000000626573752032362e372d646576656c6f702d64323033323031");
+
+ /** Signer indices exactly as one header of the first range carries them: distinct, not sorted. */
+ private static final List ARRIVAL_ORDER = List.of(2, 6, 1);
+
+ /** A second measured arrival order, from another header of the same range. */
+ private static final List ARRIVAL_ORDER_2 = List.of(1, 3, 6, 0);
+
+ private final PqAnchorConfig live =
+ new PqAnchorConfig(
+ CHAIN_ID,
+ H_LIVE,
+ Map.of(H_LIVE, 0, 13_034_000L, 3),
+ OptionalInt.empty(),
+ false,
+ OptionalInt.empty(),
+ OptionalInt.of(INTERVAL));
+
+ private final LocalRegistry registry = new LocalRegistry();
+ private final PqAnchorDigestRule r1 = new PqAnchorDigestRule(live);
+ private final PqAnchorSealsRule r2 = new PqAnchorSealsRule(live, registry);
+
+ // -----------------------------------------------------------------------------------------------
+ // The heights under test really are anchor heights. Without this the file measures nothing.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void everyHeightUnderTestIsAnAnchorHeightOfTheLiveConfiguration() {
+ for (final long height :
+ List.of(BEFORE, OFF_FIRST, OFF_MIDDLE, OFF_LAST, LOW_FIRST, LOW_MIDDLE, LOW_LAST, AFTER)) {
+ assertThat(live.anchorAppliesAt(height))
+ .describedAs("height %d must be one the rules judge, or this file proves nothing", height)
+ .isTrue();
+ assertThat(live.minSealsAt(height))
+ .describedAs("the schedule asks for three seals at height %d", height)
+ .isEqualTo(3);
+ }
+ assertThat(PqAnchorLapse.isDisarmed(OFF_FIRST)).isTrue();
+ assertThat(PqAnchorLapse.isDisarmed(OFF_LAST)).isTrue();
+ assertThat(PqAnchorLapse.isDisarmed(BEFORE)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(LOW_FIRST)).isFalse();
+ assertThat(PqAnchorLapse.historicMinSeals(LOW_FIRST)).hasValue(1);
+ assertThat(PqAnchorLapse.historicMinSeals(AFTER)).isEmpty();
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // RANGE 1, nothing in force: a header of the shape the chain holds is accepted by both rules.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void r1AcceptsAHeaderOfTheFirstRangeAsWritten() {
+ for (final long height : List.of(OFF_FIRST, OFF_MIDDLE, OFF_LAST)) {
+ final BlockHeader parent = parentHeader(height - 1L);
+ final BlockHeader block = unenforcedHeader(height, parent, ARRIVAL_ORDER);
+ assertThat(r1.validate(block, parent))
+ .describedAs(
+ "height %d carries the client version string in vanityData and a certificate in "
+ + "arrival order; that is what the chain holds, and a node syncing from genesis "
+ + "must be able to pass it",
+ height)
+ .isTrue();
+ }
+ }
+
+ @Test
+ public void r2AcceptsAHeaderOfTheFirstRangeAsWritten() {
+ for (final long height : List.of(OFF_FIRST, OFF_MIDDLE, OFF_LAST)) {
+ final BlockHeader parent = parentHeader(height - 1L);
+ final BlockHeader block = unenforcedHeader(height, parent, ARRIVAL_ORDER);
+ assertThat(r2.validate(block, parent, contextWith(VALIDATORS)))
+ .describedAs("height %d must pass the seals rule too, or the node stops 32 blocks on", height)
+ .isTrue();
+ }
+ }
+
+ @Test
+ public void bothRulesAcceptTheSecondMeasuredArrivalOrder() {
+ final BlockHeader parent = parentHeader(OFF_MIDDLE - 1L);
+ final BlockHeader block = unenforcedHeader(OFF_MIDDLE, parent, ARRIVAL_ORDER_2);
+ assertThat(r1.validate(block, parent)).isTrue();
+ assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue();
+ }
+
+ @Test
+ public void insideTheFirstRangeTheThresholdIsNotAppliedEither() {
+ // The threshold is one of the things that was switched off, so a header of this range may carry
+ // fewer seals than K. The schedule asks three; this certificate carries one.
+ final BlockHeader parent = parentHeader(OFF_FIRST - 1L);
+ final BlockHeader block = unenforcedHeader(OFF_FIRST, parent, List.of(4));
+ assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue();
+ }
+
+ @Test
+ public void insideTheFirstRangeAnHonestHeaderIsStillAccepted() {
+ // The exception must not break the ordinary case: a correctly written header that happens to
+ // fall inside the range is accepted too, so nothing has to be re-produced.
+ final BlockHeader parent = parentHeader(OFF_FIRST - 1L);
+ final BlockHeader block = honestBlock(OFF_FIRST, parent, List.of(0, 1, 2));
+ assertThat(r1.validate(block, parent)).isTrue();
+ assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue();
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // RANGE 2, only the threshold lowered: the digest and the ordering are STILL required, and the
+ // short certificate is accepted down to the floor that was really in force, and no further.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void insideTheSecondRangeTheDigestIsStillRequired() {
+ // This is the assertion that stops the first range's relaxation from leaking into the second.
+ // If it ever passes, the binding has been thrown away on 19,425 anchor heights.
+ for (final long height : List.of(LOW_FIRST, LOW_MIDDLE, LOW_LAST)) {
+ final BlockHeader parent = parentHeader(height - 1L);
+ final BlockHeader noDigest = unenforcedHeader(height, parent, List.of(0, 1, 2));
+ assertThat(r1.validate(noDigest, parent))
+ .describedAs("height %d is inside the lowered-threshold range, where the digest DID hold", height)
+ .isFalse();
+
+ // Positive control at the same height.
+ assertThat(r1.validate(honestBlock(height, parent, List.of(0, 1, 2)), parent)).isTrue();
+ }
+ }
+
+ @Test
+ public void insideTheSecondRangeTheOrderingIsStillRequired() {
+ final BlockHeader parent = parentHeader(LOW_MIDDLE - 1L);
+ final ProtocolContext context = contextWith(VALIDATORS);
+ final BlockHeader unsorted = digestBoundButUnsorted(LOW_MIDDLE, parent, ARRIVAL_ORDER);
+ assertThat(r2.validate(unsorted, parent, context)).isFalse();
+ assertThat(r2.validate(honestBlock(LOW_MIDDLE, parent, List.of(1, 2, 6)), parent, context))
+ .isTrue();
+ }
+
+ @Test
+ public void insideTheSecondRangeAShortCertificateIsAccepted() {
+ // Measured: 2,926 of the 19,425 anchor heights of this range carry one or two seals where the
+ // schedule asks three, because the fleet was running with the emergency ceiling lowered.
+ for (final long height : List.of(LOW_FIRST, LOW_MIDDLE, LOW_LAST)) {
+ final BlockHeader parent = parentHeader(height - 1L);
+ final ProtocolContext context = contextWith(VALIDATORS);
+ assertThat(live.minSealsAt(height)).isEqualTo(3);
+
+ assertThat(r2.validate(honestBlock(height, parent, List.of(0, 1)), parent, context))
+ .describedAs("two seals at height %d, the count actually written there", height)
+ .isTrue();
+ assertThat(r2.validate(honestBlock(height, parent, List.of(5)), parent, context))
+ .describedAs("one seal at height %d, the lowest count measured anywhere in the range", height)
+ .isTrue();
+ }
+ }
+
+ @Test
+ public void insideTheSecondRangeAnEmptyCertificateIsStillRefused() {
+ // The floor is ONE, not zero, because one is the ceiling that was actually in force and no
+ // header of the range carries zero. A floor of zero would accept an empty certificate where a
+ // certificate was in fact required, which is weaker than the history needs.
+ final BlockHeader parent = parentHeader(LOW_MIDDLE - 1L);
+ final ProtocolContext context = contextWith(VALIDATORS);
+ assertThat(r2.validate(honestBlock(LOW_MIDDLE, parent, List.of()), parent, context)).isFalse();
+ assertThat(r2.validate(honestBlock(LOW_MIDDLE, parent, List.of(0)), parent, context)).isTrue();
+ }
+
+ @Test
+ public void insideTheSecondRangeAnInvalidSignatureIsStillRefused() {
+ // Only the COUNT moves. Every seal still has to verify, and still has to belong to a validator
+ // of the parent.
+ final BlockHeader parent = parentHeader(LOW_MIDDLE - 1L);
+ final ProtocolContext context = contextWith(VALIDATORS);
+ final List bad = List.of(new FalconSeal(0, Bytes.repeat((byte) 0x5a, 655)));
+ final Bytes32 digest =
+ PqAnchor.anchorDigest(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(), bad);
+ final BlockHeader block = header(LOW_MIDDLE, parent.getHash(), extraData(digest, bad));
+ assertThat(r2.validate(block, parent, context)).isFalse();
+ assertThat(r2.validate(honestBlock(LOW_MIDDLE, parent, List.of(0)), parent, context)).isTrue();
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // OUTSIDE both ranges: nothing changed at all.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void outsideBothRangesAHeaderWithNoDigestIsStillRefusedByR1() {
+ for (final long height : List.of(BEFORE, LOW_FIRST, AFTER)) {
+ final BlockHeader parent = parentHeader(height - 1L);
+ final BlockHeader block = unenforcedHeader(height, parent, ARRIVAL_ORDER);
+ assertThat(r1.validate(block, parent))
+ .describedAs(
+ "height %d is outside the range where nothing was in force, so a header with no "
+ + "digest must still stop this node; that is what a NEW lapse looks like",
+ height)
+ .isFalse();
+
+ final BlockHeader honest = honestBlock(height, parent, List.of(0, 1, 2));
+ assertThat(r1.validate(honest, parent))
+ .describedAs("an honest header at height %d is still accepted", height)
+ .isTrue();
+ }
+ }
+
+ @Test
+ public void outsideBothRangesTheSameShapeIsStillRefusedByR2() {
+ for (final long height : List.of(BEFORE, LOW_FIRST, AFTER)) {
+ final BlockHeader parent = parentHeader(height - 1L);
+ final ProtocolContext context = contextWith(VALIDATORS);
+ final BlockHeader block = unenforcedHeader(height, parent, ARRIVAL_ORDER);
+ assertThat(r2.validate(block, parent, context))
+ .describedAs("height %d is outside the disarmed range, so unsorted indices are refused", height)
+ .isFalse();
+
+ final BlockHeader honest = honestBlock(height, parent, List.of(0, 1, 2));
+ assertThat(r2.validate(honest, parent, context))
+ .describedAs("an honest header at height %d is still accepted", height)
+ .isTrue();
+ }
+ }
+
+ @Test
+ public void afterBothRangesTheFullThresholdIsBack() {
+ // One anchor step past the end of the lowered range the schedule applies again, unchanged.
+ // This is the assertion that stops the lowered threshold from becoming permanent.
+ final BlockHeader parent = parentHeader(AFTER - 1L);
+ final ProtocolContext context = contextWith(VALIDATORS);
+ assertThat(live.minSealsAt(AFTER)).isEqualTo(3);
+ assertThat(r2.validate(honestBlock(AFTER, parent, List.of(0, 1)), parent, context)).isFalse();
+ assertThat(r2.validate(honestBlock(AFTER, parent, List.of(0, 1, 2)), parent, context)).isTrue();
+ }
+
+ @Test
+ public void wellBeyondTheRangesNothingIsRelaxed() {
+ final long far = 14_077_008L;
+ assertThat(live.anchorAppliesAt(far)).isTrue();
+ final BlockHeader parent = parentHeader(far - 1L);
+ final ProtocolContext context = contextWith(VALIDATORS);
+ assertThat(r1.validate(unenforcedHeader(far, parent, ARRIVAL_ORDER), parent)).isFalse();
+ assertThat(r2.validate(honestBlock(far, parent, List.of(0, 1)), parent, context)).isFalse();
+ assertThat(r2.validate(honestBlock(far, parent, List.of(0, 1, 2)), parent, context)).isTrue();
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // The bounds are exact, to one anchor step and to one block.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void theBoundsAreExactToOneAnchorStep() {
+ assertThat(BEFORE + INTERVAL).isEqualTo(OFF_FIRST);
+ assertThat(OFF_LAST + INTERVAL).isEqualTo(LOW_FIRST);
+ assertThat(LOW_LAST + INTERVAL).isEqualTo(AFTER);
+
+ final BlockHeader beforeParent = parentHeader(BEFORE - 1L);
+ assertThat(r1.validate(unenforcedHeader(BEFORE, beforeParent, ARRIVAL_ORDER), beforeParent))
+ .isFalse();
+
+ final BlockHeader firstParent = parentHeader(OFF_FIRST - 1L);
+ assertThat(r1.validate(unenforcedHeader(OFF_FIRST, firstParent, ARRIVAL_ORDER), firstParent))
+ .isTrue();
+
+ final BlockHeader lastParent = parentHeader(OFF_LAST - 1L);
+ assertThat(r1.validate(unenforcedHeader(OFF_LAST, lastParent, ARRIVAL_ORDER), lastParent))
+ .isTrue();
+
+ final BlockHeader lowParent = parentHeader(LOW_FIRST - 1L);
+ assertThat(r1.validate(unenforcedHeader(LOW_FIRST, lowParent, ARRIVAL_ORDER), lowParent))
+ .isFalse();
+
+ // And the far end of the lowered range, to one anchor step, on the threshold.
+ final BlockHeader lowLastParent = parentHeader(LOW_LAST - 1L);
+ final ProtocolContext context = contextWith(VALIDATORS);
+ assertThat(r2.validate(honestBlock(LOW_LAST, lowLastParent, List.of(0)), lowLastParent, context))
+ .isTrue();
+ final BlockHeader afterParent = parentHeader(AFTER - 1L);
+ assertThat(r2.validate(honestBlock(AFTER, afterParent, List.of(0)), afterParent, context))
+ .isFalse();
+ }
+
+ @Test
+ public void theBoundsAreExactToOneBlockOnBothRules() {
+ // The heights either side of the bounds are not anchor heights, so both rules stand down there
+ // for the ordinary reason. Asserted so that "accepted" at OFF_FIRST - 1 is not mistaken for the
+ // exception having leaked one block down.
+ assertThat(live.anchorAppliesAt(OFF_FIRST - 1L)).isFalse();
+ assertThat(live.anchorAppliesAt(OFF_LAST + 1L)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(OFF_FIRST - 1L)).isFalse();
+ assertThat(PqAnchorLapse.isDisarmed(OFF_LAST + 1L)).isFalse();
+ assertThat(PqAnchorLapse.historicMinSeals(LOW_LAST + 1L)).isEmpty();
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // What the exception deliberately does NOT relax, at any height.
+ // -----------------------------------------------------------------------------------------------
+
+ @Test
+ public void insideTheFirstRangeARepeatedIndexIsStillRefusedByBothRules() {
+ // Sortedness fixes one accepted order for a given set; distinctness is what makes "repeat one
+ // seal to inflate the count" unrepresentable. Measured on every header of the range: distinct
+ // in all of them. So this refusal costs nothing and keeps the attack unrepresentable.
+ final BlockHeader parent = parentHeader(OFF_FIRST - 1L);
+ final BlockHeader repeated = unenforcedHeader(OFF_FIRST, parent, List.of(2, 6, 2));
+ assertThat(r1.validate(repeated, parent)).isFalse();
+ assertThat(r2.validate(repeated, parent, contextWith(VALIDATORS))).isFalse();
+
+ // Positive control: the same rule at the same height accepts the distinct version.
+ final BlockHeader distinct = unenforcedHeader(OFF_FIRST, parent, ARRIVAL_ORDER);
+ assertThat(r1.validate(distinct, parent)).isTrue();
+ assertThat(r2.validate(distinct, parent, contextWith(VALIDATORS))).isTrue();
+ }
+
+ @Test
+ public void insideTheFirstRangeAnUndecodableHeaderIsStillRefused() {
+ final BlockHeader parent = parentHeader(OFF_FIRST - 1L);
+ final BlockHeader garbage =
+ header(OFF_FIRST, parent.getHash(), Bytes.fromHexString("0xdeadbeef"));
+ assertThat(r1.validate(garbage, parent)).isFalse();
+ assertThat(r2.validate(garbage, parent, contextWith(VALIDATORS))).isFalse();
+ }
+
+ @Test
+ public void belowTheActivationHeightNothingIsJudgedAtAll() {
+ // The ranges sit far above H, so this only restates the height gate; it is here because a
+ // future edit that moved the range gates ABOVE the height gate would make the rules decode
+ // headers on the whole existing chain, which is the property that lets this binary be warmed.
+ final BlockHeader parent = parentHeader(H_LIVE - 2L);
+ final BlockHeader block = unenforcedHeader(H_LIVE - 1L, parent, ARRIVAL_ORDER);
+ assertThat(r1.validate(block, parent)).isTrue();
+ assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue();
+ }
+
+ // -----------------------------------------------------------------------------------------------
+ // Helpers.
+ // -----------------------------------------------------------------------------------------------
+
+ /**
+ * A header of the shape the first range holds: the client version string in vanityData instead of
+ * a digest, and the certificate written in the order the commits arrived.
+ */
+ private BlockHeader unenforcedHeader(
+ final long number, final BlockHeader parent, final List indicesInArrivalOrder) {
+ return header(
+ number, parent.getHash(), extraData(CLIENT_VANITY, sealsFor(parent, indicesInArrivalOrder)));
+ }
+
+ /** A header whose digest is correctly bound but whose indices are left in arrival order. */
+ private BlockHeader digestBoundButUnsorted(
+ final long number, final BlockHeader parent, final List indicesInArrivalOrder) {
+ final List seals = sealsFor(parent, indicesInArrivalOrder);
+ final Bytes32 digest =
+ PqAnchor.anchorDigest(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(), seals);
+ return header(number, parent.getHash(), extraData(digest, seals));
+ }
+
+ /** A header an honest proposer under the armed rules writes: digest bound, indices sorted. */
+ private BlockHeader honestBlock(
+ final long number, final BlockHeader parent, final List indices) {
+ final List sorted = PqAnchor.sortedByIndex(sealsFor(parent, indices));
+ final Bytes32 digest =
+ PqAnchor.anchorDigest(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(), sorted);
+ return header(number, parent.getHash(), extraData(digest, sorted));
+ }
+
+ private List sealsFor(final BlockHeader parent, final List indices) {
+ // One return, one mutability. An early return of an immutable empty list here is what
+ // errorprone's MixedMutabilityReturnType refuses, and it is refusing something real: the empty
+ // case is not special, it is the case where the loop runs zero times.
+ final Bytes32 message =
+ PqAnchor.commitMessage(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes());
+ final List seals = new ArrayList<>();
+ for (final int index : indices) {
+ seals.add(new FalconSeal(index, LocalRegistry.sign(index, message)));
+ }
+ return seals;
+ }
+
+ private ProtocolContext contextWith(final Collection parentValidators) {
+ final ValidatorProvider validatorProvider =
+ mock(ValidatorProvider.class, withSettings().strictness(Strictness.LENIENT));
+ when(validatorProvider.getValidatorsForBlock(any())).thenReturn(parentValidators);
+ 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();
+ }
+
+ /**
+ * A registry of the same shape as the Falcon one, with a signature that is a deterministic
+ * function of the index AND the message, so a seal replayed at another height fails exactly as a
+ * real verification would. It exists so that the positive controls are real acceptances rather
+ * than acceptances bought by a registry that agrees to everything.
+ */
+ private static final class LocalRegistry implements PqSignerRegistry {
+
+ private final Map binding = new HashMap<>();
+
+ private LocalRegistry() {
+ for (int i = 0; i < VALIDATORS.size(); i++) {
+ binding.put(i, VALIDATORS.get(i));
+ }
+ }
+
+ static Bytes sign(final int index, final Bytes message) {
+ final byte[] out = new byte[655];
+ out[0] = (byte) index;
+ for (int i = 0; i < message.size() && i < 32; i++) {
+ out[1 + i] = message.get(i);
+ }
+ for (int i = 33; i < out.length; i++) {
+ out[i] = (byte) ((i * (index + 3) + 7) & 0xFF);
+ }
+ return Bytes.wrap(out);
+ }
+
+ @Override
+ public Address addressForIndexAtHistoric(final long blockNumber, final int validatorIndex) {
+ return binding.get(validatorIndex);
+ }
+
+ @Override
+ public boolean verifyAtHistoric(
+ final long blockNumber,
+ final int validatorIndex,
+ final Bytes message,
+ final Bytes signature) {
+ return signature != null && signature.equals(sign(validatorIndex, message));
+ }
+
+ @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 Bytes message,
+ final Bytes signature) {
+ return verifyAtHistoric(blockNumber, validatorIndex, message, signature);
+ }
+
+ @Override
+ public String toString() {
+ return "LocalRegistry";
+ }
+ }
+}
diff --git a/patches/0003-aere-pq-anchor.patch b/patches/0003-aere-pq-anchor.patch
index 9db6516..1611fb9 100644
--- a/patches/0003-aere-pq-anchor.patch
+++ b/patches/0003-aere-pq-anchor.patch
@@ -1,6 +1,6 @@
-From ce832eb0614c6029d1aaa503b3717a83b6d02734 Mon Sep 17 00:00:00 2001
+From 817da3e4ac2288e192129bbb69e8cfaaf1723812 Mon Sep 17 00:00:00 2001
From: Aere Network
-Date: Sat, 15 Aug 2026 23:43:50 +0300
+Date: Sun, 16 Aug 2026 01:24:17 +0300
Subject: [PATCH] Aere Network: post-quantum certificate anchor for QBFT
Puts a 32-byte digest of the validator certificate into vanityData, which is
@@ -27,6 +27,39 @@ above, using the verified registry; two or more blocks below the schedule
nothing changes. Covered by the new PqParentHeightAlignmentTest, and measured
on a node synced from genesis, which now crosses the first anchor height
instead of stopping one block below it.
+
+Updated 2026-08-16: names the two historical ranges on chain 2800 in which
+these rules were not fully in force, so that a node validating from genesis can
+cross them. On 2026-08-10 the whole validator set was restarted inside a few
+minutes; a node that has just restarted holds no seals until it takes part in
+one commit, so for a moment every node held none, the next height was an anchor
+height, nobody could propose and the chain stopped. The measure that restarted
+it switched these rules off across the set, and then restored them with the
+emergency seal ceiling lowered rather than at the scheduled threshold. The
+headers written meanwhile are canonical and cannot be reproduced: over the first
+range they carry the client version string in vanityData instead of the anchor
+digest and their certificates are in arrival order rather than sorted; over the
+second, much longer range they carry a correct digest and sorted indices but
+fewer seals than the schedule asks for. Nodes already holding the chain never
+revalidate them, which is why this stayed invisible; a node validating from
+genesis stopped at the first header of each range and could not pass it.
+
+The exception therefore sits at VALIDATION, where a header is accepted as it was
+written, and not in any recomputation, which would only move the error. The
+ranges are fixed constants that a node cannot widen at runtime, so a NEW lapse
+still stops the client, which is what should happen; outside them nothing
+changes at all. Neither property is monotone, so the bounds were not searched
+but measured: every anchor height from the activation height to the head was
+read one at a time and its vanityData and certificate decoded, and exactly two
+contiguous ranges appear. What the exception does not relax: over the first
+range a certificate must still decode and must still carry non-negative,
+pairwise distinct indices, so a repeated seal stays unrepresentable at every
+height; over the second range only the count moves, every seal is still verified
+against the parent's validator set, and the count keeps the floor that was
+really in force rather than dropping to zero. Covered by the new PqAnchorLapse,
+PqAnchorLapseTest and PqAnchorDisarmedWindowTest, and measured on a node synced
+from genesis, which now crosses both ranges instead of stopping at the first
+header of each.
---
.../org/hyperledger/besu/cli/BesuCommand.java | 24 +
.../cli/options/AerePqEmergencyOptions.java | 245 ++
@@ -38,8 +71,9 @@ instead of stopping one block below it.
.../consensus/common/bft/BftExtraData.java | 50 +-
.../besu/consensus/common/bft/FalconSeal.java | 88 +
.../common/bft/FalconSealSupport.java | 3749 +++++++++++++++++
- .../besu/consensus/common/bft/PqAnchor.java | 335 ++
+ .../besu/consensus/common/bft/PqAnchor.java | 339 ++
.../consensus/common/bft/PqAnchorConfig.java | 1320 ++++++
+ .../consensus/common/bft/PqAnchorLapse.java | 375 ++
.../common/bft/PqAnchorNotReadyException.java | 132 +
.../common/bft/PqAnchorSyncModeGuard.java | 130 +
.../common/bft/PqAnchorThresholdGuard.java | 252 ++
@@ -56,6 +90,7 @@ instead of stopping one block below it.
.../common/bft/PqAnchorConfigTest.java | 691 +++
.../bft/PqAnchorEmergencyConfigTest.java | 120 +
.../common/bft/PqAnchorIntervalTest.java | 186 +
+ .../common/bft/PqAnchorLapseTest.java | 249 ++
.../common/bft/PqAnchorMinSealsFloorTest.java | 153 +
.../common/bft/PqAnchorProducerCostTest.java | 251 ++
.../common/bft/PqAnchorSealCapTest.java | 150 +
@@ -90,27 +125,29 @@ instead of stopping one block below it.
.../QbftBlockCreatorFactory.java | 35 +-
.../FalconSealValidationRule.java | 501 +++
.../PqAnchorDigestAttachedRule.java | 115 +
- .../PqAnchorDigestRule.java | 207 +
- .../PqAnchorSealsRule.java | 329 ++
+ .../PqAnchorDigestRule.java | 265 ++
+ .../PqAnchorSealsRule.java | 403 ++
.../PqEmergencyShoutRule.java | 134 +
.../PqRegistryBindingRule.java | 91 +
.../qbft/QbftAnchorRuleWiringTest.java | 194 +
.../FalconSealLogThrottleTest.java | 182 +
...alconSealValidationRuleRetirementTest.java | 85 +
.../PqAnchorDigestRuleTest.java | 359 ++
+ .../PqAnchorDisarmedWindowTest.java | 548 +++
.../PqAnchorSealsRuleTest.java | 690 +++
.../PqAnchorTestSupport.java | 144 +
.../PqArmedWithoutRegistryTest.java | 185 +
.../PqEmergencyShoutRuleTest.java | 78 +
.../PqForkGateFeedTest.java | 189 +
.../eth/sync/DownloadHeadersStep.java | 103 +-
- 76 files changed, 24406 insertions(+), 45 deletions(-)
+ 79 files changed, 25714 insertions(+), 45 deletions(-)
create mode 100755 app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
create mode 100755 app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java
+ create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuard.java
@@ -126,6 +163,7 @@ instead of stopping one block below it.
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java
+ create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorProducerCostTest.java
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java
@@ -156,6 +194,7 @@ instead of stopping one block below it.
create mode 100755 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealLogThrottleTest.java
create mode 100755 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRuleRetirementTest.java
create mode 100755 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRuleTest.java
+ create mode 100755 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
create mode 100755 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java
create mode 100755 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java
create mode 100755 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java
@@ -5004,10 +5043,10 @@ index 000000000..af694dcf9
+}
diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
new file mode 100755
-index 000000000..a383bcf1c
+index 000000000..59c2d8683
--- /dev/null
+++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
-@@ -0,0 +1,335 @@
+@@ -0,0 +1,339 @@
+/*
+ * Copyright contributors to Besu / Aere Network.
+ *
@@ -5259,11 +5298,17 @@ index 000000000..a383bcf1c
+ * [13,266,800, 13,269,400] straight off the public endpoint: 82 anchor headers, 46 sorted, 36 not,
+ * none without a certificate. The 36 are CONTIGUOUS at the 32-block anchor spacing and nothing
+ * outside the window is affected, so the range below is the measured extent, not a guess.
++ *
++ * DERIVED, not written a second time. The bounds themselves live in {@link PqAnchorLapse},
++ * which is the one place a historical window is named, because the SAME interruption that left
++ * these certificates unsorted also left their headers without an anchor digest, and a value that
++ * appears in two places diverges. This constant is kept because it is the name this class has
++ * always used for the first bound.
+ */
-+ public static final long UNORDERED_WINDOW_FIRST = 13_267_824L;
++ public static final long UNORDERED_WINDOW_FIRST = PqAnchorLapse.windows().get(0).firstBlock();
+
+ /** Last anchor height of that window. Its neighbours 13,267,792 and 13,268,976 are both sorted. */
-+ public static final long UNORDERED_WINDOW_LAST = 13_268_944L;
++ public static final long UNORDERED_WINDOW_LAST = PqAnchorLapse.windows().get(0).lastBlock();
+
+ /**
+ * Whether the certificate's indices are non-negative and pairwise DISTINCT, in any order.
@@ -5325,9 +5370,7 @@ index 000000000..a383bcf1c
+ if (hasStrictlyIncreasingIndices(certificate)) {
+ return true;
+ }
-+ final boolean inHistoricalWindow =
-+ blockNumber >= UNORDERED_WINDOW_FIRST && blockNumber <= UNORDERED_WINDOW_LAST;
-+ return inHistoricalWindow && hasDistinctNonNegativeIndices(certificate);
++ return PqAnchorLapse.isDisarmed(blockNumber) && hasDistinctNonNegativeIndices(certificate);
+ }
+
+ /**
@@ -6669,6 +6712,387 @@ index 000000000..ce9f081b5
+ + '}';
+ }
+}
+diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java
+new file mode 100755
+index 000000000..a7c8c9bf1
+--- /dev/null
++++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java
+@@ -0,0 +1,375 @@
++/*
++ * 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 java.util.Comparator;
++import java.util.List;
++import java.util.Optional;
++import java.util.OptionalInt;
++
++/**
++ * The historical ranges on chain 2800 in which the anchor rules were not fully in force.
++ *
++ *
WHAT HAPPENED. On 2026-08-10 all nine validators were restarted inside a few minutes. A
++ * node that has just restarted holds no Falcon seals until it takes part in one commit, so for a
++ * moment every node held none; the very next height was an anchor height, its threshold asked for
++ * three seals, nobody could propose, and the chain stopped for nine minutes. The measure that
++ * restarted it was to switch the anchor rules off on all nine nodes at once, and then to restore
++ * them with the emergency seal ceiling lowered rather than at the scheduled threshold. That leaves
++ * TWO ranges of canonical headers, not one, and they fail a strict validator for different reasons:
++ *
++ *
++ * - The rules off entirely. vanityData carries the client's usual version string instead
++ * of the anchor digest, although the certificate is attached, so the digest binding does not
++ * hold; and the certificate's validator indices are in the order the commit messages ARRIVED
++ * rather than sorted, so they are distinct but not increasing.
++ *
- The rules on, the threshold lowered. vanityData carries a correct digest and the
++ * indices are sorted, so both of those bind exactly as they should. What differs is the COUNT:
++ * a proposer running under the lowered ceiling legitimately wrote fewer seals than the
++ * configured schedule asks for, so the header is short of the threshold and nothing else.
++ *
++ *
++ * WHY THE EXCEPTION BELONGS AT VALIDATION AND NOWHERE ELSE. Nodes that already hold these
++ * blocks never revalidate them, which is why the damage was invisible. A node built from this source
++ * and synced from genesis does validate them, and under the plain rules it stops at the first header
++ * of each range and never passes it. The history is what it is: no recomputation can turn these
++ * headers into headers that satisfy a rule they were written without. What a correct client can do
++ * is name the ranges and judge those headers by the rule that was actually in force when they were
++ * written, which is what every node holding the chain does today. A patch anywhere else moves the
++ * error instead of ending it.
++ *
++ *
THE BOUNDS, MEASURED, NOT SEARCHED. Neither property is monotone: inside the second
++ * range only about one anchor header in seven is short of the threshold and the rest are not, so a
++ * binary search would return a height that looks exactly like an answer and is not one. Every anchor
++ * height from the activation height 13,014,000 to the head at the time of measurement, 14,077,000,
++ * was therefore read one by one and its vanityData and certificate decoded: 25,252 heights,
++ * zero transport errors, and exactly the two contiguous ranges named below. Outside them there is
++ * not one header without a digest, not one with unsorted indices, and not one below the threshold.
++ *
++ *
WHAT THE RANGES DO NOT RELAX, AT ANY HEIGHT. Over the first range a certificate must
++ * still decode and must still carry non-negative, pairwise DISTINCT indices: sortedness fixes one
++ * accepted order for a given set, while distinctness is the part that is load bearing against an
++ * attacker, because it is what makes "repeat one seal to inflate the count" unrepresentable in the
++ * grammar of the format. Over the second range NOTHING is relaxed except the count, and even the
++ * count keeps a floor: the lowest number of seals measured anywhere in that range is
++ * 1, which is the ceiling that was actually in force, so that is the floor applied,
++ * not zero. Both ranges therefore admit exactly the history that exists and nothing weaker.
++ *
++ *
THE BOUNDS ARE FIXED IN CODE ON PURPOSE. A range a node could widen at runtime would let
++ * a future lapse pass unnoticed. Outside these ranges nothing changes at all, so a NEW unenforced
++ * header still stops this client, which is what should happen. Producers never consult this class:
++ * new headers are always written with the digest, with sorted indices and at the scheduled
++ * threshold, so the ranges are closed by construction and cannot grow.
++ *
++ *
THE TYPE IS A LIST BECAUSE A THIRD RANGE MUST NOT REQUIRE TOUCHING A RULE. The two
++ * entries below are the two the chain has. Adding another is an edit to this file alone.
++ */
++public final class PqAnchorLapse {
++
++ /** The chain these ranges belong to. They describe no other chain. */
++ public static final long CHAIN_ID = 2800L;
++
++ /** The anchor spacing in force over both ranges, used by the self-check below. */
++ public static final long ANCHOR_SPACING = 32L;
++
++ /** What was not in force over a range. */
++ public enum Relaxation {
++ /** Nothing was in force: no digest binding, no ordering, no threshold. */
++ EVERYTHING,
++ /** The digest and the ordering were in force; only the seal threshold was lowered. */
++ THRESHOLD_ONLY
++ }
++
++ /**
++ * One contiguous range of heights over which the anchor rules were not fully in force.
++ *
++ *
Both bounds are INCLUSIVE and both are measured anchor heights: the first is the first header
++ * that shows the defect and the last is the last one that shows it.
++ */
++ public static final class Window {
++
++ private final long firstBlock;
++ private final long lastBlock;
++ private final long anchorHeights;
++ private final Relaxation relaxation;
++ private final int effectiveMinSeals;
++ private final String reason;
++
++ /**
++ * Names a range.
++ *
++ * @param firstBlock the first affected height, inclusive
++ * @param lastBlock the last affected height, inclusive
++ * @param anchorHeights how many anchor heights the range spans; carried so that the count and
++ * the bounds are checked against each other rather than written twice
++ * @param relaxation what was not in force
++ * @param effectiveMinSeals the seal threshold that WAS in force over the range, which is the
++ * floor still applied inside it; zero when nothing was in force
++ * @param reason why the range exists, and what was measured
++ */
++ public Window(
++ final long firstBlock,
++ final long lastBlock,
++ final long anchorHeights,
++ final Relaxation relaxation,
++ final int effectiveMinSeals,
++ final String reason) {
++ if (firstBlock < 0L || lastBlock < firstBlock) {
++ throw new IllegalArgumentException(
++ "AERE PQ ANCHOR LAPSE: a window must be a non-negative, non-empty range, got ["
++ + firstBlock
++ + ", "
++ + lastBlock
++ + "]");
++ }
++ if (anchorHeights < 1L) {
++ throw new IllegalArgumentException(
++ "AERE PQ ANCHOR LAPSE: a window must span at least one anchor height, got "
++ + anchorHeights);
++ }
++ if (effectiveMinSeals < 0) {
++ throw new IllegalArgumentException(
++ "AERE PQ ANCHOR LAPSE: the threshold in force cannot be negative, got "
++ + effectiveMinSeals);
++ }
++ this.firstBlock = firstBlock;
++ this.lastBlock = lastBlock;
++ this.anchorHeights = anchorHeights;
++ this.relaxation = relaxation;
++ this.effectiveMinSeals = effectiveMinSeals;
++ this.reason = reason;
++ }
++
++ /**
++ * The first affected height.
++ *
++ * @return the inclusive lower bound
++ */
++ public long firstBlock() {
++ return firstBlock;
++ }
++
++ /**
++ * The last affected height.
++ *
++ * @return the inclusive upper bound
++ */
++ public long lastBlock() {
++ return lastBlock;
++ }
++
++ /**
++ * How many anchor heights this range spans.
++ *
++ * @return the span, in anchor heights
++ */
++ public long anchorHeights() {
++ return anchorHeights;
++ }
++
++ /**
++ * What was not in force over this range.
++ *
++ * @return the relaxation
++ */
++ public Relaxation relaxation() {
++ return relaxation;
++ }
++
++ /**
++ * The seal threshold that WAS in force over this range.
++ *
++ * @return the measured floor, zero when no rule was in force
++ */
++ public int effectiveMinSeals() {
++ return effectiveMinSeals;
++ }
++
++ /**
++ * Why this range exists.
++ *
++ * @return the reason, with what was measured
++ */
++ public String reason() {
++ return reason;
++ }
++
++ /**
++ * Whether a height falls inside this range.
++ *
++ * @param blockNumber the height being judged
++ * @return true iff the height is within the inclusive bounds
++ */
++ public boolean covers(final long blockNumber) {
++ return blockNumber >= firstBlock && blockNumber <= lastBlock;
++ }
++
++ @Override
++ public String toString() {
++ return "["
++ + firstBlock
++ + ", "
++ + lastBlock
++ + "] "
++ + relaxation
++ + " ("
++ + anchorHeights
++ + " anchor heights, threshold in force "
++ + effectiveMinSeals
++ + ")";
++ }
++ }
++
++ private static final List WINDOWS =
++ List.of(
++ new Window(
++ 13_267_824L,
++ 13_268_944L,
++ 36L,
++ Relaxation.EVERYTHING,
++ 0,
++ "2026-08-10: the anchor rules were switched off on the whole validator set to restart "
++ + "a stopped chain, so the headers produced meanwhile carry the client version "
++ + "string in vanityData instead of the anchor digest, and their certificates "
++ + "carry the indices in arrival order rather than sorted. Measured: 36 anchor "
++ + "heights, all 36 affected, all with distinct non-negative indices"),
++ new Window(
++ 13_268_976L,
++ 13_890_544L,
++ 19_425L,
++ Relaxation.THRESHOLD_ONLY,
++ 1,
++ "2026-08-10 onwards: the rules were restored immediately afterwards "
++ + "but with the emergency seal ceiling lowered, because raising the threshold "
++ + "back in one step had stopped the chain a second time. Every header here binds "
++ + "its digest and carries sorted indices; 2,926 of the 19,425 "
++ + "anchor heights in the range simply carry fewer seals than the schedule asks, "
++ + "the fewest being 1"));
++
++ static {
++ // The list is ordered and disjoint, and each entry's stated span agrees with its own bounds.
++ // This runs at class initialisation so that a future entry which contradicts itself, or which
++ // overlaps its neighbour, fails loudly at startup rather than silently widening what a node
++ // accepts.
++ Window previous = null;
++ for (final Window window : WINDOWS) {
++ if (previous != null && window.firstBlock() <= previous.lastBlock()) {
++ throw new IllegalStateException(
++ "AERE PQ ANCHOR LAPSE: windows must be ordered and disjoint, "
++ + previous
++ + " overlaps "
++ + window);
++ }
++ final long derived = (window.lastBlock() - window.firstBlock()) / ANCHOR_SPACING + 1L;
++ if (derived != window.anchorHeights()) {
++ throw new IllegalStateException(
++ "AERE PQ ANCHOR LAPSE: window "
++ + window
++ + " says it spans "
++ + window.anchorHeights()
++ + " anchor heights, but its bounds at spacing "
++ + ANCHOR_SPACING
++ + " span "
++ + derived);
++ }
++ if (window.relaxation() == Relaxation.EVERYTHING && window.effectiveMinSeals() != 0) {
++ throw new IllegalStateException(
++ "AERE PQ ANCHOR LAPSE: window " + window + " has no rule in force, so no threshold");
++ }
++ if (window.relaxation() == Relaxation.THRESHOLD_ONLY && window.effectiveMinSeals() < 1) {
++ throw new IllegalStateException(
++ "AERE PQ ANCHOR LAPSE: window "
++ + window
++ + " relaxes only the threshold, so the threshold it replaces it with must be at "
++ + "least 1; a floor of zero would accept an empty certificate where a certificate "
++ + "was in fact required");
++ }
++ previous = window;
++ }
++ }
++
++ private PqAnchorLapse() {}
++
++ /**
++ * Every named range, ordered by first height.
++ *
++ * @return an unmodifiable list, never null, possibly empty
++ */
++ public static List windows() {
++ return WINDOWS;
++ }
++
++ /**
++ * Whether the anchor rules were out of force ENTIRELY at this height.
++ *
++ * This is the single question both anchor rules ask before they judge anything. They ask it
++ * through this one method, for the same reason they share {@code anchorAppliesAt}: two rules that
++ * each decided the boundary their own way could disagree about one header, and a header accepted
++ * by one rule and refused by the other is a chain break.
++ *
++ *
It is deliberately FALSE inside a threshold-only range. There the digest binding and the
++ * ordering did hold and are still required; only {@link #historicMinSeals(long)} moves.
++ *
++ * @param blockNumber the height being judged
++ * @return true iff the height falls inside a range where nothing was in force
++ */
++ public static boolean isDisarmed(final long blockNumber) {
++ for (final Window window : WINDOWS) {
++ if (window.covers(blockNumber) && window.relaxation() == Relaxation.EVERYTHING) {
++ return true;
++ }
++ }
++ return false;
++ }
++
++ /**
++ * The seal threshold that was actually in force at this height, when it was lower than the
++ * configured schedule.
++ *
++ *
This is the same lever the fleet itself used at the time, the emergency ceiling, replayed
++ * from a fixed range instead of from a runtime option. A node validating history therefore asks
++ * the certificate for what was asked of the proposer that wrote it, and nothing more.
++ *
++ * @param blockNumber the height being judged
++ * @return the threshold in force, or empty when the configured schedule applies unchanged
++ */
++ public static OptionalInt historicMinSeals(final long blockNumber) {
++ for (final Window window : WINDOWS) {
++ if (window.covers(blockNumber) && window.relaxation() == Relaxation.THRESHOLD_ONLY) {
++ return OptionalInt.of(window.effectiveMinSeals());
++ }
++ }
++ return OptionalInt.empty();
++ }
++
++ /**
++ * The range covering a height, when there is one.
++ *
++ * @param blockNumber the height being judged
++ * @return the range, or empty
++ */
++ public static Optional windowCovering(final long blockNumber) {
++ return WINDOWS.stream().filter(w -> w.covers(blockNumber)).findFirst();
++ }
++
++ /**
++ * The first height of the earliest named range, or empty when there is none.
++ *
++ * @return the earliest affected height
++ */
++ public static Optional earliestAffectedBlock() {
++ return WINDOWS.stream().map(Window::firstBlock).min(Comparator.naturalOrder());
++ }
++}
diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java
new file mode 100755
index 000000000..772560067
@@ -13357,6 +13781,261 @@ index 000000000..08afdba0a
+ .isEqualTo(1);
+ }
+}
+diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java
+new file mode 100755
+index 000000000..038d86287
+--- /dev/null
++++ b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java
+@@ -0,0 +1,249 @@
++/*
++ * 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.util.List;
++
++import org.junit.jupiter.api.Test;
++
++/**
++ * The named historical ranges in which the anchor rules were not fully in force.
++ *
++ * The bounds are the whole content of the class under test, so they are asserted as LITERALS
++ * rather than through the constants they define. An assertion written as {@code
++ * assertThat(FIRST).isEqualTo(FIRST)} would follow any future edit of the constant and would
++ * therefore never fail, which is the exact shape of a gate that cannot go red.
++ */
++public class PqAnchorLapseTest {
++
++ private static final long LAST_ENFORCED_BEFORE = 13_267_792L;
++
++ private static final long DISARMED_FIRST = 13_267_824L;
++ private static final long DISARMED_LAST = 13_268_944L;
++
++ private static final long LOWERED_FIRST = 13_268_976L;
++ private static final long LOWERED_LAST = 13_890_544L;
++
++ private static final long FIRST_FULLY_ENFORCED_AFTER = 13_890_576L;
++
++ @Test
++ public void theListNamesExactlyTheTwoMeasuredRanges() {
++ // Measured by reading vanityData and the certificate at every anchor height from the activation
++ // height 13,014,000 to the head at the time of measurement, 14,077,000, one height at a time.
++ // The list is a LIST so a third could be added without touching a rule; it has two because the
++ // chain has two.
++ assertThat(PqAnchorLapse.windows()).hasSize(2);
++
++ final PqAnchorLapse.Window disarmed = PqAnchorLapse.windows().get(0);
++ assertThat(disarmed.firstBlock()).isEqualTo(13_267_824L);
++ assertThat(disarmed.lastBlock()).isEqualTo(13_268_944L);
++ assertThat(disarmed.anchorHeights()).isEqualTo(36L);
++ assertThat(disarmed.relaxation()).isEqualTo(PqAnchorLapse.Relaxation.EVERYTHING);
++ assertThat(disarmed.effectiveMinSeals()).isZero();
++ assertThat(disarmed.reason()).contains("2026-08-10");
++
++ final PqAnchorLapse.Window lowered = PqAnchorLapse.windows().get(1);
++ assertThat(lowered.firstBlock()).isEqualTo(13_268_976L);
++ assertThat(lowered.lastBlock()).isEqualTo(13_890_544L);
++ assertThat(lowered.anchorHeights()).isEqualTo(19_425L);
++ assertThat(lowered.relaxation()).isEqualTo(PqAnchorLapse.Relaxation.THRESHOLD_ONLY);
++ assertThat(lowered.effectiveMinSeals()).isEqualTo(1);
++ }
++
++ @Test
++ public void theTwoRangesAreAdjacentButDisjoint() {
++ // The second begins at the next anchor height after the first ends. They must not overlap, and
++ // there must be no unnamed anchor height between them, because a height in neither range is
++ // judged strictly and there is no such height here.
++ assertThat(DISARMED_LAST + PqAnchorLapse.ANCHOR_SPACING).isEqualTo(LOWERED_FIRST);
++ assertThat(PqAnchorLapse.windows().get(0).lastBlock())
++ .isLessThan(PqAnchorLapse.windows().get(1).firstBlock());
++ }
++
++ @Test
++ public void theStatedSpansAgreeWithTheBounds() {
++ // 36, not 37: (13,268,944 - 13,267,824) / 32 + 1 = 36. And 19,425 for the second range. The
++ // span is carried in each entry so the two cannot drift apart in silence; this asserts the
++ // arithmetic from the outside as well.
++ for (final PqAnchorLapse.Window window : PqAnchorLapse.windows()) {
++ assertThat((window.lastBlock() - window.firstBlock()) % PqAnchorLapse.ANCHOR_SPACING).isZero();
++ assertThat((window.lastBlock() - window.firstBlock()) / PqAnchorLapse.ANCHOR_SPACING + 1L)
++ .isEqualTo(window.anchorHeights());
++ }
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // isDisarmed: TRUE only in the first range, and deliberately FALSE in the second.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void isDisarmedIsTrueOnlyWhereNothingWasInForce() {
++ assertThat(PqAnchorLapse.isDisarmed(DISARMED_FIRST)).isTrue();
++ assertThat(PqAnchorLapse.isDisarmed(DISARMED_LAST)).isTrue();
++
++ // The neighbouring anchor heights, both measured to carry a digest, stay under the strict rules.
++ assertThat(PqAnchorLapse.isDisarmed(LAST_ENFORCED_BEFORE)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(DISARMED_FIRST - 1L)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(DISARMED_LAST + 1L)).isFalse();
++
++ // And FALSE across the whole second range: there the digest and the ordering did hold, and a
++ // relaxation that leaked into it would throw away the binding on 19,425 anchor heights.
++ assertThat(PqAnchorLapse.isDisarmed(LOWERED_FIRST)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(13_500_016L)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(LOWERED_LAST)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(FIRST_FULLY_ENFORCED_AFTER)).isFalse();
++ }
++
++ @Test
++ public void everyAnchorHeightOfTheDisarmedRangeIsCovered() {
++ long covered = 0L;
++ for (long height = DISARMED_FIRST;
++ height <= DISARMED_LAST;
++ height += PqAnchorLapse.ANCHOR_SPACING) {
++ assertThat(PqAnchorLapse.isDisarmed(height))
++ .describedAs("anchor height %d of the disarmed range", height)
++ .isTrue();
++ covered++;
++ }
++ assertThat(covered).isEqualTo(36L);
++
++ // The rest of the chain is untouched.
++ assertThat(PqAnchorLapse.isDisarmed(0L)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(13_014_000L)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(13_000_000L)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(14_077_000L)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(Long.MAX_VALUE)).isFalse();
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // historicMinSeals: the threshold that was really in force, and only where it was lower.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void historicMinSealsAnswersOnlyInsideTheLoweredRange() {
++ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST)).hasValue(1);
++ assertThat(PqAnchorLapse.historicMinSeals(13_500_016L)).hasValue(1);
++ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_LAST)).hasValue(1);
++
++ // One anchor step past the end and the configured schedule applies again, unchanged. This is
++ // the assertion that stops the lowered threshold from becoming permanent.
++ assertThat(PqAnchorLapse.historicMinSeals(FIRST_FULLY_ENFORCED_AFTER)).isEmpty();
++ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_LAST + 1L)).isEmpty();
++ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST - 1L)).isEmpty();
++ assertThat(PqAnchorLapse.historicMinSeals(LAST_ENFORCED_BEFORE)).isEmpty();
++ assertThat(PqAnchorLapse.historicMinSeals(14_077_000L)).isEmpty();
++ assertThat(PqAnchorLapse.historicMinSeals(Long.MAX_VALUE)).isEmpty();
++
++ // And it does NOT answer inside the disarmed range: there the rules are skipped whole, so a
++ // threshold would be a second, contradictory way of saying the same thing.
++ assertThat(PqAnchorLapse.historicMinSeals(DISARMED_FIRST)).isEmpty();
++ assertThat(PqAnchorLapse.historicMinSeals(DISARMED_LAST)).isEmpty();
++ }
++
++ @Test
++ public void theFloorInsideTheLoweredRangeIsOneAndNotZero() {
++ // Measured: 2,926 of the 19,425 anchor heights in that range carry fewer seals than the
++ // schedule asks, the counts seen are 1 and 2, and NOT ONE carries zero. A floor of zero would
++ // therefore accept an empty certificate at a height where a certificate was in fact required,
++ // which is weaker than the history needs.
++ assertThat(PqAnchorLapse.historicMinSeals(LOWERED_FIRST)).hasValue(1);
++ assertThat(PqAnchorLapse.windows().get(1).effectiveMinSeals()).isPositive();
++ }
++
++ @Test
++ public void windowCoveringNamesTheRangeOrNothing() {
++ assertThat(PqAnchorLapse.windowCovering(DISARMED_FIRST)).isPresent();
++ assertThat(PqAnchorLapse.windowCovering(DISARMED_FIRST).get().lastBlock())
++ .isEqualTo(DISARMED_LAST);
++ assertThat(PqAnchorLapse.windowCovering(LOWERED_FIRST)).isPresent();
++ assertThat(PqAnchorLapse.windowCovering(LOWERED_FIRST).get().relaxation())
++ .isEqualTo(PqAnchorLapse.Relaxation.THRESHOLD_ONLY);
++ assertThat(PqAnchorLapse.windowCovering(FIRST_FULLY_ENFORCED_AFTER)).isEmpty();
++ assertThat(PqAnchorLapse.earliestAffectedBlock()).contains(DISARMED_FIRST);
++ }
++
++ @Test
++ public void theDisarmedRangeIsTheSameOBJECTTheOrderingExceptionUses() {
++ // A second copy of these bounds anywhere else is a value that diverges. PqAnchor used to carry
++ // them as its own literals; it now derives them, and this is the assertion that says so.
++ assertThat(PqAnchor.UNORDERED_WINDOW_FIRST).isEqualTo(DISARMED_FIRST);
++ assertThat(PqAnchor.UNORDERED_WINDOW_LAST).isEqualTo(DISARMED_LAST);
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // The self-checks can say no. A guard that has never refused anything cannot be believed.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void aRangeThatContradictsItselfIsRefusedRatherThanAccepted() {
++ assertThatThrownBy(
++ () ->
++ new PqAnchorLapse.Window(
++ 100L, 99L, 1L, PqAnchorLapse.Relaxation.EVERYTHING, 0, "backwards"))
++ .isInstanceOf(IllegalArgumentException.class)
++ .hasMessageContaining("non-empty range");
++ assertThatThrownBy(
++ () ->
++ new PqAnchorLapse.Window(
++ -1L, 100L, 1L, PqAnchorLapse.Relaxation.EVERYTHING, 0, "negative"))
++ .isInstanceOf(IllegalArgumentException.class);
++ assertThatThrownBy(
++ () ->
++ new PqAnchorLapse.Window(
++ 100L, 200L, 0L, PqAnchorLapse.Relaxation.EVERYTHING, 0, "no heights"))
++ .isInstanceOf(IllegalArgumentException.class)
++ .hasMessageContaining("at least one anchor height");
++ assertThatThrownBy(
++ () ->
++ new PqAnchorLapse.Window(
++ 100L, 200L, 4L, PqAnchorLapse.Relaxation.THRESHOLD_ONLY, -1, "negative floor"))
++ .isInstanceOf(IllegalArgumentException.class)
++ .hasMessageContaining("cannot be negative");
++ }
++
++ @Test
++ public void aRangeCoversItsOwnBoundsAndNothingOutsideThem() {
++ final PqAnchorLapse.Window window =
++ new PqAnchorLapse.Window(
++ 1_000L, 1_032L, 2L, PqAnchorLapse.Relaxation.THRESHOLD_ONLY, 1, "probe");
++ assertThat(window.covers(1_000L)).isTrue();
++ assertThat(window.covers(1_016L)).isTrue();
++ assertThat(window.covers(1_032L)).isTrue();
++ assertThat(window.covers(999L)).isFalse();
++ assertThat(window.covers(1_033L)).isFalse();
++ assertThat(window.toString()).contains("1000").contains("1032").contains("THRESHOLD_ONLY");
++ }
++
++ @Test
++ public void theListIsUnmodifiable() {
++ // A range a node could widen at runtime would let a future lapse pass unnoticed.
++ final List windows = PqAnchorLapse.windows();
++ assertThatThrownBy(
++ () ->
++ windows.add(
++ new PqAnchorLapse.Window(
++ 1L, 2L, 1L, PqAnchorLapse.Relaxation.EVERYTHING, 0, "injected")))
++ .isInstanceOf(UnsupportedOperationException.class);
++ }
++
++ @Test
++ public void theRangesBelongToThisChainAndAreDeclaredSo() {
++ assertThat(PqAnchorLapse.CHAIN_ID).isEqualTo(2800L);
++ assertThat(PqAnchorLapse.ANCHOR_SPACING).isEqualTo(32L);
++ }
++}
diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorMinSealsFloorTest.java
new file mode 100755
index 000000000..53917eecb
@@ -22646,10 +23325,10 @@ index 000000000..b1a3ba265
+}
diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java
new file mode 100755
-index 000000000..c79a884ad
+index 000000000..ce646450c
--- /dev/null
+++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java
-@@ -0,0 +1,207 @@
+@@ -0,0 +1,265 @@
+/*
+ * Copyright contributors to Besu / Aere Network.
+ *
@@ -22671,6 +23350,7 @@ index 000000000..c79a884ad
+import org.hyperledger.besu.consensus.common.bft.FalconSeal;
+import org.hyperledger.besu.consensus.common.bft.PqAnchor;
+import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
++import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
+import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
+import org.hyperledger.besu.ethereum.core.BlockHeader;
+import org.hyperledger.besu.ethereum.mainnet.DetachedBlockHeaderValidationRule;
@@ -22780,6 +23460,16 @@ index 000000000..c79a884ad
+ return true;
+ }
+
++ // SECOND GATE: the named historical windows in which these rules were not in force. Inside one
++ // of them vanityData carries the client's version string rather than a digest, so there is no
++ // digest to match and no recomputation can produce one; refusing would stop this node forever
++ // at a height the whole network holds. See PqAnchorLapse for what happened, how the bounds were
++ // measured, and why an exception written anywhere other than validation moves the error instead
++ // of ending it.
++ if (PqAnchorLapse.isDisarmed(number)) {
++ return acceptAsWrittenInsideLapse(header, number);
++ }
++
+ try {
+ final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
+
@@ -22845,6 +23535,53 @@ index 000000000..c79a884ad
+ }
+ }
+
++ /**
++ * Accept a header from a named unenforced window as it was written, after the one check that is
++ * not relaxed at any height.
++ *
++ * Distinctness is kept and the difference is the whole reason this is not a bare {@code return
++ * true}: sortedness fixes ONE accepted order for a given set, while distinctness is what makes
++ * "repeat one seal to inflate the count" unrepresentable in the grammar of the format. Every
++ * header of the measured window has distinct, non-negative indices, so this admits exactly the
++ * history that exists and nothing weaker.
++ *
++ *
Decoding must still succeed. A header this rule cannot parse has not been shown to be one of
++ * the headers the window is about, so it is refused here as everywhere else in this file.
++ *
++ * @param header the header being judged
++ * @param number its height
++ * @return true iff the carried certificate decodes and its indices are distinct and non-negative
++ */
++ private boolean acceptAsWrittenInsideLapse(final BlockHeader header, final long number) {
++ try {
++ final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
++ final List certificate = List.copyOf(extraData.getFalconSeals());
++ if (!PqAnchor.hasDistinctNonNegativeIndices(certificate)) {
++ LOG.warn(
++ "AERE PQ ANCHOR (R1): block {} REJECTED - it falls inside the historical range {} in "
++ + "which the anchor rules were not in force, but its certificate repeats a "
++ + "validator index or carries a negative one. That is not a shape any header of "
++ + "that range has, and it is the one property the exception does not relax.",
++ number,
++ PqAnchorLapse.windowCovering(number).orElse(null));
++ return false;
++ }
++ LOG.debug(
++ "AERE PQ ANCHOR (R1): block {} accepted as written; it falls inside the historical range "
++ + "{} in which the anchor rules were not in force, so it carries no digest to bind.",
++ number,
++ PqAnchorLapse.windowCovering(number).orElse(null));
++ return true;
++ } catch (final Exception e) {
++ LOG.warn(
++ "AERE PQ ANCHOR (R1): block {} REJECTED - it falls inside a historical unenforced range "
++ + "but its extraData could not be decoded: {}",
++ number,
++ e.toString());
++ return false;
++ }
++ }
++
+ @Override
+ public boolean includeInLightValidation() {
+ return true;
@@ -22859,10 +23596,10 @@ index 000000000..c79a884ad
+}
diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
new file mode 100755
-index 000000000..a4350747a
+index 000000000..9be0c7b33
--- /dev/null
+++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
-@@ -0,0 +1,329 @@
+@@ -0,0 +1,403 @@
+/*
+ * Copyright contributors to Besu / Aere Network.
+ *
@@ -22885,6 +23622,7 @@ index 000000000..a4350747a
+import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
+import org.hyperledger.besu.consensus.common.bft.PqAnchor;
+import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
++import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
+import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
+import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
+import org.hyperledger.besu.datatypes.Address;
@@ -22895,6 +23633,7 @@ index 000000000..a4350747a
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
++import java.util.OptionalInt;
+import java.util.Set;
+
+import org.apache.tuweni.bytes.Bytes32;
@@ -22990,6 +23729,15 @@ index 000000000..a4350747a
+ return true;
+ }
+
++ // SECOND GATE, and the SAME method R1 asks, for the same reason both rules share
++ // anchorAppliesAt: if the two rules each decided this boundary their own way, a header accepted
++ // by one and refused by the other would be a chain break. Inside a named window the threshold
++ // was not in force when the header was written and the indices were written in arrival order,
++ // so neither can be required of it now.
++ if (PqAnchorLapse.isDisarmed(number)) {
++ return acceptAsWrittenInsideLapse(header, number);
++ }
++
+ try {
+ if (parent == null) {
+ LOG.warn(
@@ -23027,7 +23775,26 @@ index 000000000..a4350747a
+ final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
+ final List certificate = List.copyOf(extraData.getFalconSeals());
+ final int k = certificate.size();
-+ final int required = config.minSealsAt(number);
++
++ // THE THRESHOLD THIS HEIGHT WAS ACTUALLY JUDGED BY. Normally the configured schedule. Inside
++ // a named historical range it is the LOWER of the schedule and the emergency ceiling that was
++ // in force when these headers were written, which is the same lever the fleet itself used at
++ // the time, replayed from a fixed range instead of from a runtime option. Asking a header for
++ // more than was asked of the proposer that wrote it refuses history that no node can
++ // reproduce; asking for the ceiling rather than for nothing keeps the strongest claim the
++ // range supports, and the ceiling is a measured floor, never zero. See PqAnchorLapse.
++ int required = config.minSealsAt(number);
++ final OptionalInt inForce = PqAnchorLapse.historicMinSeals(number);
++ if (inForce.isPresent() && inForce.getAsInt() < required) {
++ LOG.debug(
++ "AERE PQ ANCHOR (R2): block {} sits in the historical range {}, so the threshold "
++ + "applied is the {} that was in force then, not the {} the schedule asks for.",
++ number,
++ PqAnchorLapse.windowCovering(number).orElse(null),
++ inForce.getAsInt(),
++ required);
++ required = inForce.getAsInt();
++ }
+
+ if (k < required) {
+ LOG.warn(
@@ -23151,6 +23918,50 @@ index 000000000..a4350747a
+ }
+
+ /**
++ * Accept a header from a named unenforced window as it was written, after the one check that is
++ * not relaxed at any height.
++ *
++ * The threshold is not applied here and neither is sortedness, because neither was applied
++ * when these headers were written; applying them now would refuse history the whole network
++ * holds. Distinctness IS applied, for the reason set out in {@link PqAnchor#hasAcceptableIndices}
++ * and in {@link PqAnchorLapse}: it is the part that is load bearing against an attacker rather
++ * than against ambiguity, and it is measured true on every header of the window.
++ *
++ * @param header the header being judged
++ * @param number its height
++ * @return true iff the carried certificate decodes and its indices are distinct and non-negative
++ */
++ private boolean acceptAsWrittenInsideLapse(final BlockHeader header, final long number) {
++ try {
++ final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
++ final List certificate = List.copyOf(extraData.getFalconSeals());
++ if (!PqAnchor.hasDistinctNonNegativeIndices(certificate)) {
++ LOG.warn(
++ "AERE PQ ANCHOR (R2): block {} REJECTED - it falls inside the historical range {} in "
++ + "which the anchor rules were not in force, but its certificate repeats a "
++ + "validator index or carries a negative one, which no header of that range does.",
++ number,
++ PqAnchorLapse.windowCovering(number).orElse(null));
++ return false;
++ }
++ LOG.debug(
++ "AERE PQ ANCHOR (R2): block {} accepted as written with {} seal(s); it falls inside the "
++ + "historical range {} in which neither the threshold nor the ordering was in force.",
++ number,
++ certificate.size(),
++ PqAnchorLapse.windowCovering(number).orElse(null));
++ return true;
++ } catch (final Exception e) {
++ LOG.warn(
++ "AERE PQ ANCHOR (R2): block {} REJECTED - it falls inside a historical unenforced range "
++ + "but its extraData could not be decoded: {}",
++ number,
++ e.toString());
++ return false;
++ }
++ }
++
++ /**
+ * AERE REGISTRY-COVERAGE REPORT: hand the parent's validator set to {@link FalconSealSupport} so
+ * its coverage report is about the height the chain is actually at. Swallows everything: a rule
+ * that rejected a header because a diagnostic threw would be a worse defect than the one this
@@ -24273,6 +25084,560 @@ index 000000000..7f03e3667
+ return header(honest.getNumber(), honest.getParentHash(), tampered);
+ }
+}
+diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
+new file mode 100755
+index 000000000..9451ec7b7
+--- /dev/null
++++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
+@@ -0,0 +1,548 @@
++/*
++ * 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.VALIDATORS;
++import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.extraData;
++import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.header;
++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.FalconSeal;
++import org.hyperledger.besu.consensus.common.bft.PqAnchor;
++import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
++import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
++import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
++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.util.ArrayList;
++import java.util.Collection;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.OptionalInt;
++
++import org.apache.tuweni.bytes.Bytes;
++import org.apache.tuweni.bytes.Bytes32;
++import org.junit.jupiter.api.Test;
++import org.mockito.quality.Strictness;
++
++/**
++ * The two historical ranges in which the anchor rules were not fully in force, as the two rules see
++ * them.
++ *
++ * The configuration here is the one the live chain runs, not the small synthetic one the other
++ * rule tests use, because the whole subject is a set of REAL heights: the activation height, the
++ * 32-block anchor spacing and the threshold schedule all have to be the production ones, or the
++ * heights under test are not anchor heights at all and every assertion below would pass on a rule
++ * that does nothing. {@link #everyHeightUnderTestIsAnAnchorHeightOfTheLiveConfiguration()} is the
++ * positive control for exactly that.
++ *
++ *
Every acceptance in this file is paired with the same header shape at a height where it must
++ * still be refused, and every refusal is paired with an honest header at that same height, which
++ * must still be accepted. Without the first pairing the tests would pass on a rule that accepts
++ * everything; without the second they would pass on a rule that refuses everything.
++ */
++public class PqAnchorDisarmedWindowTest {
++
++ /** The live activation height H. */
++ private static final long H_LIVE = 13_014_000L;
++
++ /** The live anchor spacing. */
++ private static final int INTERVAL = 32;
++
++ /** The last anchor height fully under the rules before the interruption. */
++ private static final long BEFORE = 13_267_792L;
++
++ /** First height of the range in which nothing was in force. */
++ private static final long OFF_FIRST = 13_267_824L;
++
++ /** A height in the middle of that range. */
++ private static final long OFF_MIDDLE = 13_268_016L;
++
++ /** Last height of that range. */
++ private static final long OFF_LAST = 13_268_944L;
++
++ /** First height of the range in which only the threshold was lowered. */
++ private static final long LOW_FIRST = 13_268_976L;
++
++ /** A height in the middle of that range. */
++ private static final long LOW_MIDDLE = 13_500_016L;
++
++ /** Last height of that range. */
++ private static final long LOW_LAST = 13_890_544L;
++
++ /** The first anchor height fully under the rules again. */
++ private static final long AFTER = 13_890_576L;
++
++ /**
++ * The vanityData the headers of the first range actually carry: the client's version string,
++ * which is what a node writes when the anchor is switched off. Read off a live header of the
++ * range.
++ */
++ private static final Bytes CLIENT_VANITY =
++ Bytes.fromHexString("0x00000000000000626573752032362e372d646576656c6f702d64323033323031");
++
++ /** Signer indices exactly as one header of the first range carries them: distinct, not sorted. */
++ private static final List ARRIVAL_ORDER = List.of(2, 6, 1);
++
++ /** A second measured arrival order, from another header of the same range. */
++ private static final List ARRIVAL_ORDER_2 = List.of(1, 3, 6, 0);
++
++ private final PqAnchorConfig live =
++ new PqAnchorConfig(
++ CHAIN_ID,
++ H_LIVE,
++ Map.of(H_LIVE, 0, 13_034_000L, 3),
++ OptionalInt.empty(),
++ false,
++ OptionalInt.empty(),
++ OptionalInt.of(INTERVAL));
++
++ private final LocalRegistry registry = new LocalRegistry();
++ private final PqAnchorDigestRule r1 = new PqAnchorDigestRule(live);
++ private final PqAnchorSealsRule r2 = new PqAnchorSealsRule(live, registry);
++
++ // -----------------------------------------------------------------------------------------------
++ // The heights under test really are anchor heights. Without this the file measures nothing.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void everyHeightUnderTestIsAnAnchorHeightOfTheLiveConfiguration() {
++ for (final long height :
++ List.of(BEFORE, OFF_FIRST, OFF_MIDDLE, OFF_LAST, LOW_FIRST, LOW_MIDDLE, LOW_LAST, AFTER)) {
++ assertThat(live.anchorAppliesAt(height))
++ .describedAs("height %d must be one the rules judge, or this file proves nothing", height)
++ .isTrue();
++ assertThat(live.minSealsAt(height))
++ .describedAs("the schedule asks for three seals at height %d", height)
++ .isEqualTo(3);
++ }
++ assertThat(PqAnchorLapse.isDisarmed(OFF_FIRST)).isTrue();
++ assertThat(PqAnchorLapse.isDisarmed(OFF_LAST)).isTrue();
++ assertThat(PqAnchorLapse.isDisarmed(BEFORE)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(LOW_FIRST)).isFalse();
++ assertThat(PqAnchorLapse.historicMinSeals(LOW_FIRST)).hasValue(1);
++ assertThat(PqAnchorLapse.historicMinSeals(AFTER)).isEmpty();
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // RANGE 1, nothing in force: a header of the shape the chain holds is accepted by both rules.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void r1AcceptsAHeaderOfTheFirstRangeAsWritten() {
++ for (final long height : List.of(OFF_FIRST, OFF_MIDDLE, OFF_LAST)) {
++ final BlockHeader parent = parentHeader(height - 1L);
++ final BlockHeader block = unenforcedHeader(height, parent, ARRIVAL_ORDER);
++ assertThat(r1.validate(block, parent))
++ .describedAs(
++ "height %d carries the client version string in vanityData and a certificate in "
++ + "arrival order; that is what the chain holds, and a node syncing from genesis "
++ + "must be able to pass it",
++ height)
++ .isTrue();
++ }
++ }
++
++ @Test
++ public void r2AcceptsAHeaderOfTheFirstRangeAsWritten() {
++ for (final long height : List.of(OFF_FIRST, OFF_MIDDLE, OFF_LAST)) {
++ final BlockHeader parent = parentHeader(height - 1L);
++ final BlockHeader block = unenforcedHeader(height, parent, ARRIVAL_ORDER);
++ assertThat(r2.validate(block, parent, contextWith(VALIDATORS)))
++ .describedAs("height %d must pass the seals rule too, or the node stops 32 blocks on", height)
++ .isTrue();
++ }
++ }
++
++ @Test
++ public void bothRulesAcceptTheSecondMeasuredArrivalOrder() {
++ final BlockHeader parent = parentHeader(OFF_MIDDLE - 1L);
++ final BlockHeader block = unenforcedHeader(OFF_MIDDLE, parent, ARRIVAL_ORDER_2);
++ assertThat(r1.validate(block, parent)).isTrue();
++ assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue();
++ }
++
++ @Test
++ public void insideTheFirstRangeTheThresholdIsNotAppliedEither() {
++ // The threshold is one of the things that was switched off, so a header of this range may carry
++ // fewer seals than K. The schedule asks three; this certificate carries one.
++ final BlockHeader parent = parentHeader(OFF_FIRST - 1L);
++ final BlockHeader block = unenforcedHeader(OFF_FIRST, parent, List.of(4));
++ assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue();
++ }
++
++ @Test
++ public void insideTheFirstRangeAnHonestHeaderIsStillAccepted() {
++ // The exception must not break the ordinary case: a correctly written header that happens to
++ // fall inside the range is accepted too, so nothing has to be re-produced.
++ final BlockHeader parent = parentHeader(OFF_FIRST - 1L);
++ final BlockHeader block = honestBlock(OFF_FIRST, parent, List.of(0, 1, 2));
++ assertThat(r1.validate(block, parent)).isTrue();
++ assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue();
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // RANGE 2, only the threshold lowered: the digest and the ordering are STILL required, and the
++ // short certificate is accepted down to the floor that was really in force, and no further.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void insideTheSecondRangeTheDigestIsStillRequired() {
++ // This is the assertion that stops the first range's relaxation from leaking into the second.
++ // If it ever passes, the binding has been thrown away on 19,425 anchor heights.
++ for (final long height : List.of(LOW_FIRST, LOW_MIDDLE, LOW_LAST)) {
++ final BlockHeader parent = parentHeader(height - 1L);
++ final BlockHeader noDigest = unenforcedHeader(height, parent, List.of(0, 1, 2));
++ assertThat(r1.validate(noDigest, parent))
++ .describedAs("height %d is inside the lowered-threshold range, where the digest DID hold", height)
++ .isFalse();
++
++ // Positive control at the same height.
++ assertThat(r1.validate(honestBlock(height, parent, List.of(0, 1, 2)), parent)).isTrue();
++ }
++ }
++
++ @Test
++ public void insideTheSecondRangeTheOrderingIsStillRequired() {
++ final BlockHeader parent = parentHeader(LOW_MIDDLE - 1L);
++ final ProtocolContext context = contextWith(VALIDATORS);
++ final BlockHeader unsorted = digestBoundButUnsorted(LOW_MIDDLE, parent, ARRIVAL_ORDER);
++ assertThat(r2.validate(unsorted, parent, context)).isFalse();
++ assertThat(r2.validate(honestBlock(LOW_MIDDLE, parent, List.of(1, 2, 6)), parent, context))
++ .isTrue();
++ }
++
++ @Test
++ public void insideTheSecondRangeAShortCertificateIsAccepted() {
++ // Measured: 2,926 of the 19,425 anchor heights of this range carry one or two seals where the
++ // schedule asks three, because the fleet was running with the emergency ceiling lowered.
++ for (final long height : List.of(LOW_FIRST, LOW_MIDDLE, LOW_LAST)) {
++ final BlockHeader parent = parentHeader(height - 1L);
++ final ProtocolContext context = contextWith(VALIDATORS);
++ assertThat(live.minSealsAt(height)).isEqualTo(3);
++
++ assertThat(r2.validate(honestBlock(height, parent, List.of(0, 1)), parent, context))
++ .describedAs("two seals at height %d, the count actually written there", height)
++ .isTrue();
++ assertThat(r2.validate(honestBlock(height, parent, List.of(5)), parent, context))
++ .describedAs("one seal at height %d, the lowest count measured anywhere in the range", height)
++ .isTrue();
++ }
++ }
++
++ @Test
++ public void insideTheSecondRangeAnEmptyCertificateIsStillRefused() {
++ // The floor is ONE, not zero, because one is the ceiling that was actually in force and no
++ // header of the range carries zero. A floor of zero would accept an empty certificate where a
++ // certificate was in fact required, which is weaker than the history needs.
++ final BlockHeader parent = parentHeader(LOW_MIDDLE - 1L);
++ final ProtocolContext context = contextWith(VALIDATORS);
++ assertThat(r2.validate(honestBlock(LOW_MIDDLE, parent, List.of()), parent, context)).isFalse();
++ assertThat(r2.validate(honestBlock(LOW_MIDDLE, parent, List.of(0)), parent, context)).isTrue();
++ }
++
++ @Test
++ public void insideTheSecondRangeAnInvalidSignatureIsStillRefused() {
++ // Only the COUNT moves. Every seal still has to verify, and still has to belong to a validator
++ // of the parent.
++ final BlockHeader parent = parentHeader(LOW_MIDDLE - 1L);
++ final ProtocolContext context = contextWith(VALIDATORS);
++ final List bad = List.of(new FalconSeal(0, Bytes.repeat((byte) 0x5a, 655)));
++ final Bytes32 digest =
++ PqAnchor.anchorDigest(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(), bad);
++ final BlockHeader block = header(LOW_MIDDLE, parent.getHash(), extraData(digest, bad));
++ assertThat(r2.validate(block, parent, context)).isFalse();
++ assertThat(r2.validate(honestBlock(LOW_MIDDLE, parent, List.of(0)), parent, context)).isTrue();
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // OUTSIDE both ranges: nothing changed at all.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void outsideBothRangesAHeaderWithNoDigestIsStillRefusedByR1() {
++ for (final long height : List.of(BEFORE, LOW_FIRST, AFTER)) {
++ final BlockHeader parent = parentHeader(height - 1L);
++ final BlockHeader block = unenforcedHeader(height, parent, ARRIVAL_ORDER);
++ assertThat(r1.validate(block, parent))
++ .describedAs(
++ "height %d is outside the range where nothing was in force, so a header with no "
++ + "digest must still stop this node; that is what a NEW lapse looks like",
++ height)
++ .isFalse();
++
++ final BlockHeader honest = honestBlock(height, parent, List.of(0, 1, 2));
++ assertThat(r1.validate(honest, parent))
++ .describedAs("an honest header at height %d is still accepted", height)
++ .isTrue();
++ }
++ }
++
++ @Test
++ public void outsideBothRangesTheSameShapeIsStillRefusedByR2() {
++ for (final long height : List.of(BEFORE, LOW_FIRST, AFTER)) {
++ final BlockHeader parent = parentHeader(height - 1L);
++ final ProtocolContext context = contextWith(VALIDATORS);
++ final BlockHeader block = unenforcedHeader(height, parent, ARRIVAL_ORDER);
++ assertThat(r2.validate(block, parent, context))
++ .describedAs("height %d is outside the disarmed range, so unsorted indices are refused", height)
++ .isFalse();
++
++ final BlockHeader honest = honestBlock(height, parent, List.of(0, 1, 2));
++ assertThat(r2.validate(honest, parent, context))
++ .describedAs("an honest header at height %d is still accepted", height)
++ .isTrue();
++ }
++ }
++
++ @Test
++ public void afterBothRangesTheFullThresholdIsBack() {
++ // One anchor step past the end of the lowered range the schedule applies again, unchanged.
++ // This is the assertion that stops the lowered threshold from becoming permanent.
++ final BlockHeader parent = parentHeader(AFTER - 1L);
++ final ProtocolContext context = contextWith(VALIDATORS);
++ assertThat(live.minSealsAt(AFTER)).isEqualTo(3);
++ assertThat(r2.validate(honestBlock(AFTER, parent, List.of(0, 1)), parent, context)).isFalse();
++ assertThat(r2.validate(honestBlock(AFTER, parent, List.of(0, 1, 2)), parent, context)).isTrue();
++ }
++
++ @Test
++ public void wellBeyondTheRangesNothingIsRelaxed() {
++ final long far = 14_077_008L;
++ assertThat(live.anchorAppliesAt(far)).isTrue();
++ final BlockHeader parent = parentHeader(far - 1L);
++ final ProtocolContext context = contextWith(VALIDATORS);
++ assertThat(r1.validate(unenforcedHeader(far, parent, ARRIVAL_ORDER), parent)).isFalse();
++ assertThat(r2.validate(honestBlock(far, parent, List.of(0, 1)), parent, context)).isFalse();
++ assertThat(r2.validate(honestBlock(far, parent, List.of(0, 1, 2)), parent, context)).isTrue();
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // The bounds are exact, to one anchor step and to one block.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void theBoundsAreExactToOneAnchorStep() {
++ assertThat(BEFORE + INTERVAL).isEqualTo(OFF_FIRST);
++ assertThat(OFF_LAST + INTERVAL).isEqualTo(LOW_FIRST);
++ assertThat(LOW_LAST + INTERVAL).isEqualTo(AFTER);
++
++ final BlockHeader beforeParent = parentHeader(BEFORE - 1L);
++ assertThat(r1.validate(unenforcedHeader(BEFORE, beforeParent, ARRIVAL_ORDER), beforeParent))
++ .isFalse();
++
++ final BlockHeader firstParent = parentHeader(OFF_FIRST - 1L);
++ assertThat(r1.validate(unenforcedHeader(OFF_FIRST, firstParent, ARRIVAL_ORDER), firstParent))
++ .isTrue();
++
++ final BlockHeader lastParent = parentHeader(OFF_LAST - 1L);
++ assertThat(r1.validate(unenforcedHeader(OFF_LAST, lastParent, ARRIVAL_ORDER), lastParent))
++ .isTrue();
++
++ final BlockHeader lowParent = parentHeader(LOW_FIRST - 1L);
++ assertThat(r1.validate(unenforcedHeader(LOW_FIRST, lowParent, ARRIVAL_ORDER), lowParent))
++ .isFalse();
++
++ // And the far end of the lowered range, to one anchor step, on the threshold.
++ final BlockHeader lowLastParent = parentHeader(LOW_LAST - 1L);
++ final ProtocolContext context = contextWith(VALIDATORS);
++ assertThat(r2.validate(honestBlock(LOW_LAST, lowLastParent, List.of(0)), lowLastParent, context))
++ .isTrue();
++ final BlockHeader afterParent = parentHeader(AFTER - 1L);
++ assertThat(r2.validate(honestBlock(AFTER, afterParent, List.of(0)), afterParent, context))
++ .isFalse();
++ }
++
++ @Test
++ public void theBoundsAreExactToOneBlockOnBothRules() {
++ // The heights either side of the bounds are not anchor heights, so both rules stand down there
++ // for the ordinary reason. Asserted so that "accepted" at OFF_FIRST - 1 is not mistaken for the
++ // exception having leaked one block down.
++ assertThat(live.anchorAppliesAt(OFF_FIRST - 1L)).isFalse();
++ assertThat(live.anchorAppliesAt(OFF_LAST + 1L)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(OFF_FIRST - 1L)).isFalse();
++ assertThat(PqAnchorLapse.isDisarmed(OFF_LAST + 1L)).isFalse();
++ assertThat(PqAnchorLapse.historicMinSeals(LOW_LAST + 1L)).isEmpty();
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // What the exception deliberately does NOT relax, at any height.
++ // -----------------------------------------------------------------------------------------------
++
++ @Test
++ public void insideTheFirstRangeARepeatedIndexIsStillRefusedByBothRules() {
++ // Sortedness fixes one accepted order for a given set; distinctness is what makes "repeat one
++ // seal to inflate the count" unrepresentable. Measured on every header of the range: distinct
++ // in all of them. So this refusal costs nothing and keeps the attack unrepresentable.
++ final BlockHeader parent = parentHeader(OFF_FIRST - 1L);
++ final BlockHeader repeated = unenforcedHeader(OFF_FIRST, parent, List.of(2, 6, 2));
++ assertThat(r1.validate(repeated, parent)).isFalse();
++ assertThat(r2.validate(repeated, parent, contextWith(VALIDATORS))).isFalse();
++
++ // Positive control: the same rule at the same height accepts the distinct version.
++ final BlockHeader distinct = unenforcedHeader(OFF_FIRST, parent, ARRIVAL_ORDER);
++ assertThat(r1.validate(distinct, parent)).isTrue();
++ assertThat(r2.validate(distinct, parent, contextWith(VALIDATORS))).isTrue();
++ }
++
++ @Test
++ public void insideTheFirstRangeAnUndecodableHeaderIsStillRefused() {
++ final BlockHeader parent = parentHeader(OFF_FIRST - 1L);
++ final BlockHeader garbage =
++ header(OFF_FIRST, parent.getHash(), Bytes.fromHexString("0xdeadbeef"));
++ assertThat(r1.validate(garbage, parent)).isFalse();
++ assertThat(r2.validate(garbage, parent, contextWith(VALIDATORS))).isFalse();
++ }
++
++ @Test
++ public void belowTheActivationHeightNothingIsJudgedAtAll() {
++ // The ranges sit far above H, so this only restates the height gate; it is here because a
++ // future edit that moved the range gates ABOVE the height gate would make the rules decode
++ // headers on the whole existing chain, which is the property that lets this binary be warmed.
++ final BlockHeader parent = parentHeader(H_LIVE - 2L);
++ final BlockHeader block = unenforcedHeader(H_LIVE - 1L, parent, ARRIVAL_ORDER);
++ assertThat(r1.validate(block, parent)).isTrue();
++ assertThat(r2.validate(block, parent, contextWith(VALIDATORS))).isTrue();
++ }
++
++ // -----------------------------------------------------------------------------------------------
++ // Helpers.
++ // -----------------------------------------------------------------------------------------------
++
++ /**
++ * A header of the shape the first range holds: the client version string in vanityData instead of
++ * a digest, and the certificate written in the order the commits arrived.
++ */
++ private BlockHeader unenforcedHeader(
++ final long number, final BlockHeader parent, final List indicesInArrivalOrder) {
++ return header(
++ number, parent.getHash(), extraData(CLIENT_VANITY, sealsFor(parent, indicesInArrivalOrder)));
++ }
++
++ /** A header whose digest is correctly bound but whose indices are left in arrival order. */
++ private BlockHeader digestBoundButUnsorted(
++ final long number, final BlockHeader parent, final List indicesInArrivalOrder) {
++ final List seals = sealsFor(parent, indicesInArrivalOrder);
++ final Bytes32 digest =
++ PqAnchor.anchorDigest(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(), seals);
++ return header(number, parent.getHash(), extraData(digest, seals));
++ }
++
++ /** A header an honest proposer under the armed rules writes: digest bound, indices sorted. */
++ private BlockHeader honestBlock(
++ final long number, final BlockHeader parent, final List indices) {
++ final List sorted = PqAnchor.sortedByIndex(sealsFor(parent, indices));
++ final Bytes32 digest =
++ PqAnchor.anchorDigest(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(), sorted);
++ return header(number, parent.getHash(), extraData(digest, sorted));
++ }
++
++ private List sealsFor(final BlockHeader parent, final List indices) {
++ // One return, one mutability. An early return of an immutable empty list here is what
++ // errorprone's MixedMutabilityReturnType refuses, and it is refusing something real: the empty
++ // case is not special, it is the case where the loop runs zero times.
++ final Bytes32 message =
++ PqAnchor.commitMessage(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes());
++ final List seals = new ArrayList<>();
++ for (final int index : indices) {
++ seals.add(new FalconSeal(index, LocalRegistry.sign(index, message)));
++ }
++ return seals;
++ }
++
++ private ProtocolContext contextWith(final Collection parentValidators) {
++ final ValidatorProvider validatorProvider =
++ mock(ValidatorProvider.class, withSettings().strictness(Strictness.LENIENT));
++ when(validatorProvider.getValidatorsForBlock(any())).thenReturn(parentValidators);
++ 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();
++ }
++
++ /**
++ * A registry of the same shape as the Falcon one, with a signature that is a deterministic
++ * function of the index AND the message, so a seal replayed at another height fails exactly as a
++ * real verification would. It exists so that the positive controls are real acceptances rather
++ * than acceptances bought by a registry that agrees to everything.
++ */
++ private static final class LocalRegistry implements PqSignerRegistry {
++
++ private final Map binding = new HashMap<>();
++
++ private LocalRegistry() {
++ for (int i = 0; i < VALIDATORS.size(); i++) {
++ binding.put(i, VALIDATORS.get(i));
++ }
++ }
++
++ static Bytes sign(final int index, final Bytes message) {
++ final byte[] out = new byte[655];
++ out[0] = (byte) index;
++ for (int i = 0; i < message.size() && i < 32; i++) {
++ out[1 + i] = message.get(i);
++ }
++ for (int i = 33; i < out.length; i++) {
++ out[i] = (byte) ((i * (index + 3) + 7) & 0xFF);
++ }
++ return Bytes.wrap(out);
++ }
++
++ @Override
++ public Address addressForIndexAtHistoric(final long blockNumber, final int validatorIndex) {
++ return binding.get(validatorIndex);
++ }
++
++ @Override
++ public boolean verifyAtHistoric(
++ final long blockNumber,
++ final int validatorIndex,
++ final Bytes message,
++ final Bytes signature) {
++ return signature != null && signature.equals(sign(validatorIndex, message));
++ }
++
++ @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 Bytes message,
++ final Bytes signature) {
++ return verifyAtHistoric(blockNumber, validatorIndex, message, signature);
++ }
++
++ @Override
++ public String toString() {
++ return "LocalRegistry";
++ }
++ }
++}
diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java
new file mode 100755
index 000000000..3bf64c2a5