A package-built node has now imported chain 2800 from block 0 to the live tip, with zero anchor rejections

Reaching the tip needed two historical facts encoded, and the second one was
not in the original hypothesis.

First, the disarmed window: 36 anchor heights (13,267,824 to 13,268,944) whose
blocks carry an attached certificate but no digest, and whose signer indices are
not strictly increasing, produced while anchoring was disarmed fleet-wide to
unblock the chain after an incident. Encoded as a named list of historical
windows with fixed bounds in code, after the precedent of the base-fee lapse
windows: inside, the header is accepted as written; outside, nothing changes.

Second, the recovery, found by measuring rather than guessing. After the
emergency re-arm the fleet ran for a stretch with a lowered seal threshold, so
proposers legitimately wrote shorter certificates. Its extent was measured by
reading 25,252 anchor heights one at a time, with NO binary search, because the
property is not monotone: only about 15 percent of heights are affected and the
largest gap between two affected heights is 44, so a binary search would have
returned an answer that looks exactly like a good one. Result: 13,268,976 to
13,890,544, thresholds measured at 1 or 2, never 0.

The second window relaxes the seal COUNT only. Digest binding and index
ordering stay enforced and every seal is still verified. Widening the disarmed
window to cover both would have been easier and would have thrown away
certificate binding on 19,425 anchor heights, which is the one property the
anchor exists for.

Negative control in four directions, because an exception can fail both ways:
the window predicate forced always-false turns 10 tests red; forced always-true
turns 39 red, of which 22 are pre-existing strictness tests, making the
exception swallowing the chain visible; the historical threshold emptied turns 7
red; pinned at 1 turns 8 red. Restored: 641 tests, 0 failures, counted from XML.

Patch verified on a pristine upstream checkout, alone and in series, and the
resulting tree compiles.
This commit is contained in:
Aere Network 2026-08-16 02:00:01 +03:00
parent d96ae52430
commit 94438cd8f1
8 changed files with 2736 additions and 24 deletions

View File

@ -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 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 poarta deja. Reparatia e in lucru; pana e dovedita prin import dincolo de fereastra, aceasta
sectiune spune limita in loc sa o ascunda. 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.

View File

@ -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, * [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 * 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. * outside the window is affected, so the range below is the measured extent, not a guess.
*
* <p>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. */ /** 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. * 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)) { if (hasStrictlyIncreasingIndices(certificate)) {
return true; return true;
} }
final boolean inHistoricalWindow = return PqAnchorLapse.isDisarmed(blockNumber) && hasDistinctNonNegativeIndices(certificate);
blockNumber >= UNORDERED_WINDOW_FIRST && blockNumber <= UNORDERED_WINDOW_LAST;
return inHistoricalWindow && hasDistinctNonNegativeIndices(certificate);
} }
/** /**

View File

@ -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.
*
* <p><b>WHAT HAPPENED.</b> 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:
*
* <ol>
* <li><b>The rules off entirely.</b> 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.
* <li><b>The rules on, the threshold lowered.</b> 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.
* </ol>
*
* <p><b>WHY THE EXCEPTION BELONGS AT VALIDATION AND NOWHERE ELSE.</b> 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.
*
* <p><b>THE BOUNDS, MEASURED, NOT SEARCHED.</b> 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.
*
* <p><b>WHAT THE RANGES DO NOT RELAX, AT ANY HEIGHT.</b> 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.
*
* <p><b>THE BOUNDS ARE FIXED IN CODE ON PURPOSE.</b> 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.
*
* <p><b>THE TYPE IS A LIST BECAUSE A THIRD RANGE MUST NOT REQUIRE TOUCHING A RULE.</b> 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.
*
* <p>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<Window> 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<Window> windows() {
return WINDOWS;
}
/**
* Whether the anchor rules were out of force ENTIRELY at this height.
*
* <p>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.
*
* <p>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.
*
* <p>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<Window> 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<Long> earliestAffectedBlock() {
return WINDOWS.stream().map(Window::firstBlock).min(Comparator.naturalOrder());
}
}

View File

@ -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.
*
* <p>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<PqAnchorLapse.Window> 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);
}
}

View File

@ -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.FalconSeal;
import org.hyperledger.besu.consensus.common.bft.PqAnchor; import org.hyperledger.besu.consensus.common.bft.PqAnchor;
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig; 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.consensus.qbft.QbftExtraDataCodec;
import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.mainnet.DetachedBlockHeaderValidationRule; import org.hyperledger.besu.ethereum.mainnet.DetachedBlockHeaderValidationRule;
@ -128,6 +129,16 @@ public class PqAnchorDigestRule implements DetachedBlockHeaderValidationRule {
return true; 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 { try {
final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData()); 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.
*
* <p>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.
*
* <p>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<FalconSeal> 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 @Override
public boolean includeInLightValidation() { public boolean includeInLightValidation() {
return true; return true;

View File

@ -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.FalconSealSupport;
import org.hyperledger.besu.consensus.common.bft.PqAnchor; import org.hyperledger.besu.consensus.common.bft.PqAnchor;
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig; 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.bft.PqSignerRegistry;
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Address;
@ -30,6 +31,7 @@ import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.OptionalInt;
import java.util.Set; import java.util.Set;
import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.bytes.Bytes32;
@ -125,6 +127,15 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
return true; 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 { try {
if (parent == null) { if (parent == null) {
LOG.warn( LOG.warn(
@ -162,7 +173,26 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData()); final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
final List<FalconSeal> certificate = List.copyOf(extraData.getFalconSeals()); final List<FalconSeal> certificate = List.copyOf(extraData.getFalconSeals());
final int k = certificate.size(); 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) { if (k < required) {
LOG.warn( 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.
*
* <p>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<FalconSeal> 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 * 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 * its coverage report is about the height the chain is actually at. Swallows everything: a rule

View File

@ -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.
*
* <p>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.
*
* <p>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<Integer> ARRIVAL_ORDER = List.of(2, 6, 1);
/** A second measured arrival order, from another header of the same range. */
private static final List<Integer> 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<FalconSeal> 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<Integer> 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<Integer> indicesInArrivalOrder) {
final List<FalconSeal> 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<Integer> indices) {
final List<FalconSeal> 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<FalconSeal> sealsFor(final BlockHeader parent, final List<Integer> 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<FalconSeal> seals = new ArrayList<>();
for (final int index : indices) {
seals.add(new FalconSeal(index, LocalRegistry.sign(index, message)));
}
return seals;
}
private ProtocolContext contextWith(final Collection<Address> 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<Integer, Address> 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";
}
}
}

File diff suppressed because it is too large Load Diff