Patch 0003 regenerated with the schedule-boundary fix: a package-built node now crosses the first real anchor
The published patch carried a one-block boundary defect: the seal rule asks the index-to-address map at the parent height (H-1) while the registry schedule binds inclusively from H, so at the very first anchor the schedule was empty and the lookup fell back to a head map that is empty on a node synced from genesis. A valid certificate was rejected. Fixed in both lookup paths: when the schedule has an entry at exactly blockNumber+1, answer from that entry's verified bound registry. The boundary stays exactly one block. Proven by import, not by assertion: the package-built node was stuck at 13,013,999 with 6,646 rejections; with the fix it crossed 13,014,000 in 42 seconds and imported 253,729 further blocks through the anchor era with zero rejections. Tests: new PqParentHeightAlignmentTest 3/3, consensus:common 391/0, consensus:qbft 217/0. Negative control measured: disarm the condition and the measuring test goes red; restore it and it is green. Patch verified to apply cleanly on a pristine upstream checkout, alone and in series with 0001/0004/0005; file list is the previous 75 plus the new test, and every untouched section is byte-identical to the old patch. The anchor/ source copy in this package was brought to the same state so the two cannot diverge. Honest limit, recorded in IMPORT-PROOF-STARE: further along, at 13,267,729, the node stops again for a DIFFERENT reason. Blocks in a band there carry an attached certificate whose vanityData is the ordinary client string rather than the digest, at heights a single static interval treats as anchor heights. The best-supported reading is that the fleet ran a different anchoring configuration in that window (these values are not consensus-bound), so the package needs a HISTORICAL anchoring schedule, not one value. Stated rather than implied.
This commit is contained in:
parent
85747e23af
commit
67e3c26e1c
@ -38,3 +38,33 @@ carpeala in focul serii.
|
||||
**RUN-A-NODE.md poarta deja avertismentul corect:** aplica 0001+0003+0004+0005 si trateaza
|
||||
build-ul ca urmaritor pana la o dovada de import completa. Aceasta dovada de import a ajuns pana
|
||||
la prima ancora si a gasit exact veriga care lipseste. Constatarea intra in registru.
|
||||
|
||||
## Adus la zi 15 august, noaptea
|
||||
|
||||
- **Cauza, gasita si dovedita in aceeasi seara:** un decalaj de un bloc la granita orarului.
|
||||
Regula sigiliilor judeca certificatul purtat de blocul primei ancore H intreband registrul la
|
||||
inaltimea PARINTELUI, H-1, fiindca aceea e inaltimea peste care se semneaza sigiliile. Orarul
|
||||
registrelor leaga insa INCLUSIV de la H, deci la H-1 nu leaga nimic, si ambele cautari pe
|
||||
inaltime cadeau pe registrul de cap, care pe un nod sincronizat de la geneza e gol. Nu era o
|
||||
divergenta de continut a maparii index catre adresa, cum banuia sectiunea de mai sus: era
|
||||
intrebarea pusa cu un bloc mai jos decat singura intrare care putea raspunde.
|
||||
- **Reparatia minima, in FalconSealSupport, pe amandoua drumurile (cheie si adresa):** cand
|
||||
orarul are o intrare la EXACT blockNumber+1, raspunsul vine din registrul legat si verificat
|
||||
al acelei intrari, nu din fisierul de cap neverificat. Granita e exact un bloc: cu doua sau
|
||||
mai multe blocuri sub orar nu se schimba nimic.
|
||||
- **Dovada, masurata, nu presupusa:** nodul construit din pachete a trecut de 13.014.000 si a
|
||||
mers peste 25.000 de blocuri fara nicio respingere, trecand si de treapta cu prag 3 de la
|
||||
13.034.000. Suitele modulelor de consens: 608 teste, zero esecuri, inclusiv testul nou
|
||||
PqParentHeightAlignmentTest, construit pe forma nodului public (cap gol, registrul doar ca
|
||||
istorie). Control negativ facut: cu conditia dezarmata, exact proba granitei iese rosie; cu
|
||||
ea la loc, verde.
|
||||
- **Peticul 0003 publicat poarta acum reparatia**, regenerat din amonte pristin, cu aceeasi
|
||||
lista de fisiere plus testul nou, si se aplica curat in seria 0001, 0003, 0004, 0005.
|
||||
Avertismentul din RUN-A-NODE.md ramane pana la o dovada de import completa pana la varf.
|
||||
- **Unde a ajuns rularea dupa reparatie, scris cinstit:** nodul a mers de la 13.014.000 pana la
|
||||
13.267.729, adica 253.729 de blocuri prin era ancorei, cu zero respingeri de certificat, si
|
||||
s-a oprit acolo la o granita NOUA, separata de cea reparata: blocuri din banda urmatoare
|
||||
poarta certificat atasat dar vanityData e sirul obisnuit al clientului, nu digestul, la
|
||||
inaltimi pe care configuratia statica a nodului le considera inaltimi de ancora, si regula
|
||||
digestului le refuza. Productia a acceptat acele blocuri la vremea lor, deci pachetul public
|
||||
inca nu reproduce si acest interval; e urmatoarea veriga de investigat, cu aceeasi metoda.
|
||||
|
||||
@ -3442,6 +3442,25 @@ public final class FalconSealSupport {
|
||||
final Optional<PqRegistryHash.ScheduleEntry> required =
|
||||
PqRegistryHash.requiredHashAt(schedule, blockNumber);
|
||||
if (required.isEmpty()) {
|
||||
// SCHEDULE BOUNDARY (2026-08-15). The only historical question ever asked one block
|
||||
// below the schedule's first entry is about the certificate carried by the block at
|
||||
// blockNumber+1, whose governing registry is the one bound EXACTLY at blockNumber+1.
|
||||
// Answer it from that entry's VERIFIED bound registry, never from the unverified head
|
||||
// file. Exactly one block: at blockNumber+2 below the schedule nothing changes.
|
||||
final Optional<PqRegistryHash.ScheduleEntry> nextEntry =
|
||||
PqRegistryHash.requiredHashAt(schedule, blockNumber + 1);
|
||||
if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) {
|
||||
final Optional<PqRegistryHash.Registry> boundNext =
|
||||
PqRegistryHash.registryAt(schedule, set, blockNumber + 1);
|
||||
if (boundNext.isPresent()) {
|
||||
for (final PqRegistryHash.Entry e : boundNext.get().entries()) {
|
||||
if (e.index() == validatorIndex) {
|
||||
return new FalconPublicKeyParameters(FalconParameters.falcon_512, e.publicKey());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return headRegistryKeyOrRefuse(
|
||||
blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
|
||||
}
|
||||
@ -3493,6 +3512,27 @@ public final class FalconSealSupport {
|
||||
blockNumber, validatorIndex, "no schedule was ever loaded", historic);
|
||||
}
|
||||
if (PqRegistryHash.requiredHashAt(schedule, blockNumber).isEmpty()) {
|
||||
// SCHEDULE BOUNDARY (2026-08-15). Same alignment as in keyAt: one block below the
|
||||
// schedule's first entry the subject is the certificate of the block at blockNumber+1,
|
||||
// governed by the registry bound EXACTLY there, so the answer comes from that entry's
|
||||
// VERIFIED bound registry and mirrors what this method answers at blockNumber+1 itself.
|
||||
final Optional<PqRegistryHash.ScheduleEntry> nextEntry =
|
||||
PqRegistryHash.requiredHashAt(schedule, blockNumber + 1);
|
||||
if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) {
|
||||
final Optional<PqRegistryHash.Registry> boundNext =
|
||||
PqRegistryHash.registryAt(schedule, set, blockNumber + 1);
|
||||
if (boundNext.isPresent()) {
|
||||
if (!boundNext.get().addressBound()) {
|
||||
return null;
|
||||
}
|
||||
for (final PqRegistryHash.Entry e : boundNext.get().entries()) {
|
||||
if (e.index() == validatorIndex && e.address() != null) {
|
||||
return Address.wrap(Bytes.wrap(e.address()));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return headRegistryAddressOrRefuse(
|
||||
blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
|
||||
}
|
||||
|
||||
@ -0,0 +1,229 @@
|
||||
/*
|
||||
* 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 org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||
import org.hyperledger.besu.datatypes.Address;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
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.bouncycastle.crypto.digests.KeccakDigest;
|
||||
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
|
||||
import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
/**
|
||||
* SCHEDULE BOUNDARY (2026-08-15): the PARENT-HEIGHT question at the first anchor.
|
||||
*
|
||||
* <p>WHAT WAS MEASURED, on a public node synced from genesis on the live chain. {@code
|
||||
* PqAnchorSealsRule} judges the certificate carried by the block at the first anchor height H by
|
||||
* asking the registry at the PARENT's height H-1, because that is the height the seals commit to.
|
||||
* The schedule binds INCLUSIVELY from H, so at H-1 it binds nothing, and both height-resolved
|
||||
* lookups fell back to the HEAD registry - which on a public node synced from genesis is EMPTY. A
|
||||
* perfectly valid certificate was refused with "certificate carries index 0, which the registry
|
||||
* does not bind to any validator address", and the node parked one block below the first anchor
|
||||
* forever (measured: head 13,013,999 = H-1, 6,646 R2 rejections in the log).
|
||||
*
|
||||
* <p>THE REPAIR UNDER TEST. In the branch where {@code requiredHashAt(schedule, blockNumber)} is
|
||||
* empty, before the head fallback: when the schedule has an entry at EXACTLY {@code blockNumber+1},
|
||||
* answer from that entry's VERIFIED bound registry. The only historical question ever asked at H-1
|
||||
* is about the certificate carried by the block at H, whose governing registry is the one bound at
|
||||
* H; the alignment uses the verified v2 registry, not an unverified head file.
|
||||
*
|
||||
* <p>THE FIXTURE IS THE PUBLIC-NODE SHAPE, DELIBERATELY: no {@code aere.falcon.genesis}, so the
|
||||
* head registry is EMPTY, and the registry reaches the node ONLY as history ({@code
|
||||
* aere.falcon.registry.history}) bound by hash to the schedule entry at H. That emptiness is what
|
||||
* makes the negative control possible - with a populated head, the H-1 assertions would pass
|
||||
* through the old head fallback and prove nothing about the repair. {@link
|
||||
* #baselineTheHeadIsEmptyOtherwiseNothingHereMeansAnything()} pins that shape.
|
||||
*/
|
||||
public class PqParentHeightAlignmentTest {
|
||||
|
||||
/** Anchor activation height H: the schedule's FIRST entry sits exactly here. */
|
||||
private static final long H = 1_000L;
|
||||
|
||||
/** Height from which the staged threshold is non-zero, i.e. the fully armed regime. */
|
||||
private static final long K_AT = H + 10L;
|
||||
|
||||
private static final int N = 7;
|
||||
|
||||
private static final long CHAIN_ID = 220_878L;
|
||||
|
||||
@TempDir private Path tmp;
|
||||
|
||||
private final List<FalconPrivateKeyParameters> privateKeys = new ArrayList<>();
|
||||
private final List<Address> validators = new ArrayList<>();
|
||||
private Path genesisPath;
|
||||
|
||||
/** A fixed 32-byte message, standing in for M(parent) of the first anchor block. */
|
||||
private static final Bytes32 MESSAGE = Bytes32.fromHexString("0x" + "5a".repeat(32));
|
||||
|
||||
private Bytes sealByIndexZero;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws Exception {
|
||||
// The same v2 proof-bound manifest as PqRegistryHeightRefusalTest, bound at H.
|
||||
final KeccakDigest kd = new KeccakDigest(256);
|
||||
final StringBuilder manifest = new StringBuilder();
|
||||
manifest
|
||||
.append("{\"config\":{\"aereFalconRegistry\":{")
|
||||
.append(PqV2Fixture.manifestHeader(N, CHAIN_ID, H));
|
||||
for (int i = 0; i < N; i++) {
|
||||
privateKeys.add(PqV2Fixture.privateKey(i));
|
||||
validators.add(PqV2Fixture.address(i));
|
||||
final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i);
|
||||
kd.update(anchoredRow, 0, anchoredRow.length);
|
||||
manifest.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, H));
|
||||
}
|
||||
manifest
|
||||
.append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
|
||||
.append("0".repeat(64))
|
||||
.append("\":\"0x");
|
||||
final byte[] anchoredHash = new byte[32];
|
||||
kd.doFinal(anchoredHash, 0);
|
||||
manifest.append(Bytes.wrap(anchoredHash).toUnprefixedHexString()).append("\"}}}}");
|
||||
|
||||
genesisPath = tmp.resolve("genesis-d228.json");
|
||||
Files.writeString(genesisPath, manifest.toString());
|
||||
// DELIBERATELY NOT setting aere.falcon.genesis: the head registry stays EMPTY, which is the
|
||||
// public-node shape the defect was measured on.
|
||||
|
||||
// A genuine Falcon-512 signature by index 0 over MESSAGE.
|
||||
final FalconSigner signer = new FalconSigner();
|
||||
signer.init(true, privateKeys.get(0));
|
||||
sealByIndexZero = Bytes.wrap(signer.generateSignature(MESSAGE.toArray()));
|
||||
|
||||
resetFalconSingleton();
|
||||
// ARMED at H, exactly like the live fleet at its first anchor height.
|
||||
PqAnchorProducer.useConfigForTesting(
|
||||
new PqAnchorConfig(CHAIN_ID, H, Map.of(H, 0, K_AT, 3), OptionalInt.empty(), false));
|
||||
|
||||
// The registry reaches this node ONLY as history, exactly like the public package does it.
|
||||
System.setProperty(
|
||||
FalconSealSupport.PROPERTY_REGISTRY_HISTORY, genesisPath.toAbsolutePath().toString());
|
||||
final FalconSealSupport pqc = FalconSealSupport.instance();
|
||||
final PqRegistryHash.Registry held = PqRegistryHash.loadAuto(genesisPath);
|
||||
final PqRegistryHash.Schedule schedule =
|
||||
scheduleFromGenesis(Map.of(H, PqRegistryHash.hashFor(held, CHAIN_ID)));
|
||||
pqc.verifyRegistryBindingOrAbort(0L, CHAIN_ID, schedule);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() throws Exception {
|
||||
System.clearProperty("aere.falcon.genesis");
|
||||
System.clearProperty("aere.pq.genesis");
|
||||
System.clearProperty(FalconSealSupport.PROPERTY_REGISTRY_HISTORY);
|
||||
resetFalconSingleton();
|
||||
PqAnchorProducer.useConfigForTesting(null);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// 0. The fixture itself: the head must be EMPTY, or the two tests below pass through the old
|
||||
// head fallback and the negative control cannot go red.
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void baselineTheHeadIsEmptyOtherwiseNothingHereMeansAnything() {
|
||||
final FalconSealSupport pqc = FalconSealSupport.instance();
|
||||
assertThat(pqc.addressForIndex(0))
|
||||
.describedAs("the HEAD registry must be empty: this is the public-node shape")
|
||||
.isNull();
|
||||
assertThat(pqc.verify(0, MESSAGE, sealByIndexZero))
|
||||
.describedAs("a head verify must fail for the same reason")
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// 1. THE MEASUREMENT. At H-1, the question is about the certificate of the block at H, and the
|
||||
// registry bound at H answers it.
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void atTheParentOfTheFirstAnchorTheBoundRegistryAnswers() {
|
||||
final FalconSealSupport pqc = FalconSealSupport.instance();
|
||||
assertThat(pqc.addressForIndexAtHistoric(H - 1L, 0))
|
||||
.describedAs(
|
||||
"SCHEDULE BOUNDARY: PqAnchorSealsRule asks at the PARENT height H-1 about the certificate carried "
|
||||
+ "by the block at H. The registry governing that certificate is the one bound at "
|
||||
+ "H, and it is VERIFIED; refusing here parks a syncing node at H-1 forever")
|
||||
.isEqualTo(validators.get(0));
|
||||
assertThat(pqc.verifyAtHistoric(H - 1L, 0, MESSAGE, sealByIndexZero))
|
||||
.describedAs("and the key half must answer from the same bound registry")
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// 2. THE BOUNDARY IS EXACTLY ONE BLOCK. At H-2 and below, nothing changes: the head fallback
|
||||
// still answers, and on this empty head that answer is a refusal, exactly as before.
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
public void theBoundaryIsExactlyOneBlock() {
|
||||
final FalconSealSupport pqc = FalconSealSupport.instance();
|
||||
assertThat(pqc.addressForIndexAtHistoric(H - 2L, 0))
|
||||
.describedAs("H-2 is not the parent of any anchor-governed height: old behaviour, refusal")
|
||||
.isNull();
|
||||
assertThat(pqc.verifyAtHistoric(H - 2L, 0, MESSAGE, sealByIndexZero)).isFalse();
|
||||
assertThat(pqc.addressForIndexAtHistoric(0L, 0))
|
||||
.describedAs("and deep below, the same")
|
||||
.isNull();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
// Helpers, borrowed from PqRegistryHeightRefusalTest.
|
||||
// -------------------------------------------------------------------------------------------
|
||||
|
||||
private PqRegistryHash.Schedule scheduleFromGenesis(final Map<Long, String> entries)
|
||||
throws Exception {
|
||||
final List<Long> heights = new ArrayList<>(entries.keySet());
|
||||
heights.sort(Long::compare);
|
||||
final StringBuilder sb = new StringBuilder("{\"config\":{\"pqRegistryHash\":[");
|
||||
for (int i = 0; i < heights.size(); i++) {
|
||||
if (i > 0) {
|
||||
sb.append(',');
|
||||
}
|
||||
final long b = heights.get(i);
|
||||
String h = entries.get(b);
|
||||
if (!h.startsWith("0x")) {
|
||||
h = "0x" + h;
|
||||
}
|
||||
sb.append("{\"block\":").append(b).append(",\"hash\":\"").append(h).append("\"}");
|
||||
}
|
||||
sb.append("]}}");
|
||||
final Path p =
|
||||
tmp.resolve("genesis-schedule-" + heights.size() + "-" + heights.get(0) + ".json");
|
||||
Files.writeString(p, sb.toString());
|
||||
return PqRegistryHash.loadScheduleFromGenesis(p);
|
||||
}
|
||||
|
||||
private static void resetFalconSingleton() throws Exception {
|
||||
final Field f = FalconSealSupport.class.getDeclaredField("instance");
|
||||
f.setAccessible(true);
|
||||
f.set(null, null);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
From 050489f7245fd9e1fa982e844b6984a3eba45d29 Mon Sep 17 00:00:00 2001
|
||||
From ce832eb0614c6029d1aaa503b3717a83b6d02734 Mon Sep 17 00:00:00 2001
|
||||
From: Aere Network <node@aere.network>
|
||||
Date: Wed, 12 Aug 2026 02:25:12 +0300
|
||||
Date: Sat, 15 Aug 2026 23:43:50 +0300
|
||||
Subject: [PATCH] Aere Network: post-quantum certificate anchor for QBFT
|
||||
|
||||
Puts a 32-byte digest of the validator certificate into vanityData, which is
|
||||
@ -15,6 +15,18 @@ same Apache License 2.0; the upstream copyright headers are left as found.
|
||||
Consensus on chain 2800 remains classical secp256k1 ECDSA. This binds a
|
||||
post-quantum certificate to the block hash; it does not make consensus
|
||||
post-quantum.
|
||||
|
||||
Updated 2026-08-15: repairs the schedule boundary at the first anchor height.
|
||||
The seals rule resolves the certificate carried by the block at height H
|
||||
against the registry at the parent height H-1, and the height-to-registry
|
||||
schedule binds inclusively from H, so at H-1 both height-resolved lookups fell
|
||||
back to the head registry, which is empty on a node syncing from genesis, and
|
||||
a valid certificate was refused one block below the first anchor. Both lookups
|
||||
now answer that one question from the schedule entry bound exactly one block
|
||||
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.
|
||||
---
|
||||
.../org/hyperledger/besu/cli/BesuCommand.java | 24 +
|
||||
.../cli/options/AerePqEmergencyOptions.java | 245 ++
|
||||
@ -25,14 +37,14 @@ post-quantum.
|
||||
.../common/bft/BftBlockInterface.java | 22 +-
|
||||
.../consensus/common/bft/BftExtraData.java | 50 +-
|
||||
.../besu/consensus/common/bft/FalconSeal.java | 88 +
|
||||
.../common/bft/FalconSealSupport.java | 3709 +++++++++++++++++
|
||||
.../common/bft/FalconSealSupport.java | 3749 +++++++++++++++++
|
||||
.../besu/consensus/common/bft/PqAnchor.java | 335 ++
|
||||
.../consensus/common/bft/PqAnchorConfig.java | 1320 ++++++
|
||||
.../common/bft/PqAnchorNotReadyException.java | 132 +
|
||||
.../common/bft/PqAnchorSyncModeGuard.java | 130 +
|
||||
.../common/bft/PqAnchorThresholdGuard.java | 252 ++
|
||||
.../common/bft/PqRegistryBinding.java | 497 +++
|
||||
.../consensus/common/bft/PqRegistryHash.java | 2341 +++++++++++
|
||||
.../consensus/common/bft/PqRegistryHash.java | 2341 ++++++++++
|
||||
.../common/bft/PqRegistryHashTool.java | 482 +++
|
||||
.../consensus/common/bft/PqSealCache.java | 351 ++
|
||||
.../consensus/common/bft/PqSealStore.java | 384 ++
|
||||
@ -40,7 +52,7 @@ post-quantum.
|
||||
.../blockcreation/BftBlockCreatorFactory.java | 25 +-
|
||||
.../bft/blockcreation/PqAnchorProducer.java | 347 ++
|
||||
.../common/bft/tools/PqRegistryHashTool.java | 231 +
|
||||
.../common/bft/FalconAttachIntervalTest.java | 232 ++
|
||||
.../common/bft/FalconAttachIntervalTest.java | 232 +
|
||||
.../common/bft/PqAnchorConfigTest.java | 691 +++
|
||||
.../bft/PqAnchorEmergencyConfigTest.java | 120 +
|
||||
.../common/bft/PqAnchorIntervalTest.java | 186 +
|
||||
@ -56,13 +68,14 @@ post-quantum.
|
||||
.../bft/PqForkThresholdReachabilityTest.java | 355 ++
|
||||
.../bft/PqForkValidatorSetChangeTest.java | 427 ++
|
||||
.../common/bft/PqInertBinaryTest.java | 475 +++
|
||||
.../bft/PqParentHeightAlignmentTest.java | 229 +
|
||||
.../common/bft/PqRegistryBindingTest.java | 595 +++
|
||||
.../bft/PqRegistryHeightRefusalTest.java | 328 ++
|
||||
.../common/bft/PqRegistryRotationTest.java | 476 +++
|
||||
.../common/bft/PqSealPersistenceTest.java | 621 +++
|
||||
.../common/bft/PqSignedHeightTest.java | 403 ++
|
||||
.../common/bft/PqStartupHistoryTest.java | 325 ++
|
||||
.../consensus/common/bft/PqV2Fixture.java | 233 ++
|
||||
.../consensus/common/bft/PqV2Fixture.java | 233 +
|
||||
.../qbft/core/messagewrappers/Commit.java | 18 +
|
||||
.../core/network/QbftMessageTransmitter.java | 25 +-
|
||||
.../qbft/core/payload/CommitPayload.java | 147 +-
|
||||
@ -91,7 +104,7 @@ post-quantum.
|
||||
.../PqEmergencyShoutRuleTest.java | 78 +
|
||||
.../PqForkGateFeedTest.java | 189 +
|
||||
.../eth/sync/DownloadHeadersStep.java | 103 +-
|
||||
75 files changed, 24137 insertions(+), 45 deletions(-)
|
||||
76 files changed, 24406 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
|
||||
@ -125,6 +138,7 @@ post-quantum.
|
||||
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkThresholdReachabilityTest.java
|
||||
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqForkValidatorSetChangeTest.java
|
||||
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqInertBinaryTest.java
|
||||
create mode 100644 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java
|
||||
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
|
||||
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHeightRefusalTest.java
|
||||
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
|
||||
@ -1235,10 +1249,10 @@ index 000000000..578b6f5aa
|
||||
+}
|
||||
diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
|
||||
new file mode 100755
|
||||
index 000000000..b042e1589
|
||||
index 000000000..af694dcf9
|
||||
--- /dev/null
|
||||
+++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
|
||||
@@ -0,0 +1,3709 @@
|
||||
@@ -0,0 +1,3749 @@
|
||||
+/*
|
||||
+ * Copyright contributors to Besu.
|
||||
+ *
|
||||
@ -4683,6 +4697,25 @@ index 000000000..b042e1589
|
||||
+ final Optional<PqRegistryHash.ScheduleEntry> required =
|
||||
+ PqRegistryHash.requiredHashAt(schedule, blockNumber);
|
||||
+ if (required.isEmpty()) {
|
||||
+ // SCHEDULE BOUNDARY (2026-08-15). The only historical question ever asked one block
|
||||
+ // below the schedule's first entry is about the certificate carried by the block at
|
||||
+ // blockNumber+1, whose governing registry is the one bound EXACTLY at blockNumber+1.
|
||||
+ // Answer it from that entry's VERIFIED bound registry, never from the unverified head
|
||||
+ // file. Exactly one block: at blockNumber+2 below the schedule nothing changes.
|
||||
+ final Optional<PqRegistryHash.ScheduleEntry> nextEntry =
|
||||
+ PqRegistryHash.requiredHashAt(schedule, blockNumber + 1);
|
||||
+ if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) {
|
||||
+ final Optional<PqRegistryHash.Registry> boundNext =
|
||||
+ PqRegistryHash.registryAt(schedule, set, blockNumber + 1);
|
||||
+ if (boundNext.isPresent()) {
|
||||
+ for (final PqRegistryHash.Entry e : boundNext.get().entries()) {
|
||||
+ if (e.index() == validatorIndex) {
|
||||
+ return new FalconPublicKeyParameters(FalconParameters.falcon_512, e.publicKey());
|
||||
+ }
|
||||
+ }
|
||||
+ return null;
|
||||
+ }
|
||||
+ }
|
||||
+ return headRegistryKeyOrRefuse(
|
||||
+ blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
|
||||
+ }
|
||||
@ -4734,6 +4767,27 @@ index 000000000..b042e1589
|
||||
+ blockNumber, validatorIndex, "no schedule was ever loaded", historic);
|
||||
+ }
|
||||
+ if (PqRegistryHash.requiredHashAt(schedule, blockNumber).isEmpty()) {
|
||||
+ // SCHEDULE BOUNDARY (2026-08-15). Same alignment as in keyAt: one block below the
|
||||
+ // schedule's first entry the subject is the certificate of the block at blockNumber+1,
|
||||
+ // governed by the registry bound EXACTLY there, so the answer comes from that entry's
|
||||
+ // VERIFIED bound registry and mirrors what this method answers at blockNumber+1 itself.
|
||||
+ final Optional<PqRegistryHash.ScheduleEntry> nextEntry =
|
||||
+ PqRegistryHash.requiredHashAt(schedule, blockNumber + 1);
|
||||
+ if (nextEntry.isPresent() && nextEntry.get().block() == blockNumber + 1) {
|
||||
+ final Optional<PqRegistryHash.Registry> boundNext =
|
||||
+ PqRegistryHash.registryAt(schedule, set, blockNumber + 1);
|
||||
+ if (boundNext.isPresent()) {
|
||||
+ if (!boundNext.get().addressBound()) {
|
||||
+ return null;
|
||||
+ }
|
||||
+ for (final PqRegistryHash.Entry e : boundNext.get().entries()) {
|
||||
+ if (e.index() == validatorIndex && e.address() != null) {
|
||||
+ return Address.wrap(Bytes.wrap(e.address()));
|
||||
+ }
|
||||
+ }
|
||||
+ return null;
|
||||
+ }
|
||||
+ }
|
||||
+ return headRegistryAddressOrRefuse(
|
||||
+ blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
|
||||
+ }
|
||||
@ -17234,6 +17288,241 @@ index 000000000..f8a26fed3
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java
|
||||
new file mode 100644
|
||||
index 000000000..0067f4b49
|
||||
--- /dev/null
|
||||
+++ b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqParentHeightAlignmentTest.java
|
||||
@@ -0,0 +1,229 @@
|
||||
+/*
|
||||
+ * 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 org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||
+import org.hyperledger.besu.datatypes.Address;
|
||||
+
|
||||
+import java.lang.reflect.Field;
|
||||
+import java.nio.file.Files;
|
||||
+import java.nio.file.Path;
|
||||
+import java.util.ArrayList;
|
||||
+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.bouncycastle.crypto.digests.KeccakDigest;
|
||||
+import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
|
||||
+import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
|
||||
+import org.junit.jupiter.api.AfterEach;
|
||||
+import org.junit.jupiter.api.BeforeEach;
|
||||
+import org.junit.jupiter.api.Test;
|
||||
+import org.junit.jupiter.api.io.TempDir;
|
||||
+
|
||||
+/**
|
||||
+ * SCHEDULE BOUNDARY (2026-08-15): the PARENT-HEIGHT question at the first anchor.
|
||||
+ *
|
||||
+ * <p>WHAT WAS MEASURED, on a public node synced from genesis on the live chain. {@code
|
||||
+ * PqAnchorSealsRule} judges the certificate carried by the block at the first anchor height H by
|
||||
+ * asking the registry at the PARENT's height H-1, because that is the height the seals commit to.
|
||||
+ * The schedule binds INCLUSIVELY from H, so at H-1 it binds nothing, and both height-resolved
|
||||
+ * lookups fell back to the HEAD registry - which on a public node synced from genesis is EMPTY. A
|
||||
+ * perfectly valid certificate was refused with "certificate carries index 0, which the registry
|
||||
+ * does not bind to any validator address", and the node parked one block below the first anchor
|
||||
+ * forever (measured: head 13,013,999 = H-1, 6,646 R2 rejections in the log).
|
||||
+ *
|
||||
+ * <p>THE REPAIR UNDER TEST. In the branch where {@code requiredHashAt(schedule, blockNumber)} is
|
||||
+ * empty, before the head fallback: when the schedule has an entry at EXACTLY {@code blockNumber+1},
|
||||
+ * answer from that entry's VERIFIED bound registry. The only historical question ever asked at H-1
|
||||
+ * is about the certificate carried by the block at H, whose governing registry is the one bound at
|
||||
+ * H; the alignment uses the verified v2 registry, not an unverified head file.
|
||||
+ *
|
||||
+ * <p>THE FIXTURE IS THE PUBLIC-NODE SHAPE, DELIBERATELY: no {@code aere.falcon.genesis}, so the
|
||||
+ * head registry is EMPTY, and the registry reaches the node ONLY as history ({@code
|
||||
+ * aere.falcon.registry.history}) bound by hash to the schedule entry at H. That emptiness is what
|
||||
+ * makes the negative control possible - with a populated head, the H-1 assertions would pass
|
||||
+ * through the old head fallback and prove nothing about the repair. {@link
|
||||
+ * #baselineTheHeadIsEmptyOtherwiseNothingHereMeansAnything()} pins that shape.
|
||||
+ */
|
||||
+public class PqParentHeightAlignmentTest {
|
||||
+
|
||||
+ /** Anchor activation height H: the schedule's FIRST entry sits exactly here. */
|
||||
+ private static final long H = 1_000L;
|
||||
+
|
||||
+ /** Height from which the staged threshold is non-zero, i.e. the fully armed regime. */
|
||||
+ private static final long K_AT = H + 10L;
|
||||
+
|
||||
+ private static final int N = 7;
|
||||
+
|
||||
+ private static final long CHAIN_ID = 220_878L;
|
||||
+
|
||||
+ @TempDir private Path tmp;
|
||||
+
|
||||
+ private final List<FalconPrivateKeyParameters> privateKeys = new ArrayList<>();
|
||||
+ private final List<Address> validators = new ArrayList<>();
|
||||
+ private Path genesisPath;
|
||||
+
|
||||
+ /** A fixed 32-byte message, standing in for M(parent) of the first anchor block. */
|
||||
+ private static final Bytes32 MESSAGE = Bytes32.fromHexString("0x" + "5a".repeat(32));
|
||||
+
|
||||
+ private Bytes sealByIndexZero;
|
||||
+
|
||||
+ @BeforeEach
|
||||
+ public void setUp() throws Exception {
|
||||
+ // The same v2 proof-bound manifest as PqRegistryHeightRefusalTest, bound at H.
|
||||
+ final KeccakDigest kd = new KeccakDigest(256);
|
||||
+ final StringBuilder manifest = new StringBuilder();
|
||||
+ manifest
|
||||
+ .append("{\"config\":{\"aereFalconRegistry\":{")
|
||||
+ .append(PqV2Fixture.manifestHeader(N, CHAIN_ID, H));
|
||||
+ for (int i = 0; i < N; i++) {
|
||||
+ privateKeys.add(PqV2Fixture.privateKey(i));
|
||||
+ validators.add(PqV2Fixture.address(i));
|
||||
+ final byte[] anchoredRow = PqV2Fixture.anchorPreimageRow(i);
|
||||
+ kd.update(anchoredRow, 0, anchoredRow.length);
|
||||
+ manifest.append(',').append(PqV2Fixture.manifestEntry(i, N, CHAIN_ID, H));
|
||||
+ }
|
||||
+ manifest
|
||||
+ .append("}},\"alloc\":{\"0000000000000000000000000000000000000fa1\":{\"storage\":{\"0x")
|
||||
+ .append("0".repeat(64))
|
||||
+ .append("\":\"0x");
|
||||
+ final byte[] anchoredHash = new byte[32];
|
||||
+ kd.doFinal(anchoredHash, 0);
|
||||
+ manifest.append(Bytes.wrap(anchoredHash).toUnprefixedHexString()).append("\"}}}}");
|
||||
+
|
||||
+ genesisPath = tmp.resolve("genesis-d228.json");
|
||||
+ Files.writeString(genesisPath, manifest.toString());
|
||||
+ // DELIBERATELY NOT setting aere.falcon.genesis: the head registry stays EMPTY, which is the
|
||||
+ // public-node shape the defect was measured on.
|
||||
+
|
||||
+ // A genuine Falcon-512 signature by index 0 over MESSAGE.
|
||||
+ final FalconSigner signer = new FalconSigner();
|
||||
+ signer.init(true, privateKeys.get(0));
|
||||
+ sealByIndexZero = Bytes.wrap(signer.generateSignature(MESSAGE.toArray()));
|
||||
+
|
||||
+ resetFalconSingleton();
|
||||
+ // ARMED at H, exactly like the live fleet at its first anchor height.
|
||||
+ PqAnchorProducer.useConfigForTesting(
|
||||
+ new PqAnchorConfig(CHAIN_ID, H, Map.of(H, 0, K_AT, 3), OptionalInt.empty(), false));
|
||||
+
|
||||
+ // The registry reaches this node ONLY as history, exactly like the public package does it.
|
||||
+ System.setProperty(
|
||||
+ FalconSealSupport.PROPERTY_REGISTRY_HISTORY, genesisPath.toAbsolutePath().toString());
|
||||
+ final FalconSealSupport pqc = FalconSealSupport.instance();
|
||||
+ final PqRegistryHash.Registry held = PqRegistryHash.loadAuto(genesisPath);
|
||||
+ final PqRegistryHash.Schedule schedule =
|
||||
+ scheduleFromGenesis(Map.of(H, PqRegistryHash.hashFor(held, CHAIN_ID)));
|
||||
+ pqc.verifyRegistryBindingOrAbort(0L, CHAIN_ID, schedule);
|
||||
+ }
|
||||
+
|
||||
+ @AfterEach
|
||||
+ public void tearDown() throws Exception {
|
||||
+ System.clearProperty("aere.falcon.genesis");
|
||||
+ System.clearProperty("aere.pq.genesis");
|
||||
+ System.clearProperty(FalconSealSupport.PROPERTY_REGISTRY_HISTORY);
|
||||
+ resetFalconSingleton();
|
||||
+ PqAnchorProducer.useConfigForTesting(null);
|
||||
+ }
|
||||
+
|
||||
+ // -------------------------------------------------------------------------------------------
|
||||
+ // 0. The fixture itself: the head must be EMPTY, or the two tests below pass through the old
|
||||
+ // head fallback and the negative control cannot go red.
|
||||
+ // -------------------------------------------------------------------------------------------
|
||||
+
|
||||
+ @Test
|
||||
+ public void baselineTheHeadIsEmptyOtherwiseNothingHereMeansAnything() {
|
||||
+ final FalconSealSupport pqc = FalconSealSupport.instance();
|
||||
+ assertThat(pqc.addressForIndex(0))
|
||||
+ .describedAs("the HEAD registry must be empty: this is the public-node shape")
|
||||
+ .isNull();
|
||||
+ assertThat(pqc.verify(0, MESSAGE, sealByIndexZero))
|
||||
+ .describedAs("a head verify must fail for the same reason")
|
||||
+ .isFalse();
|
||||
+ }
|
||||
+
|
||||
+ // -------------------------------------------------------------------------------------------
|
||||
+ // 1. THE MEASUREMENT. At H-1, the question is about the certificate of the block at H, and the
|
||||
+ // registry bound at H answers it.
|
||||
+ // -------------------------------------------------------------------------------------------
|
||||
+
|
||||
+ @Test
|
||||
+ public void atTheParentOfTheFirstAnchorTheBoundRegistryAnswers() {
|
||||
+ final FalconSealSupport pqc = FalconSealSupport.instance();
|
||||
+ assertThat(pqc.addressForIndexAtHistoric(H - 1L, 0))
|
||||
+ .describedAs(
|
||||
+ "SCHEDULE BOUNDARY: PqAnchorSealsRule asks at the PARENT height H-1 about the certificate carried "
|
||||
+ + "by the block at H. The registry governing that certificate is the one bound at "
|
||||
+ + "H, and it is VERIFIED; refusing here parks a syncing node at H-1 forever")
|
||||
+ .isEqualTo(validators.get(0));
|
||||
+ assertThat(pqc.verifyAtHistoric(H - 1L, 0, MESSAGE, sealByIndexZero))
|
||||
+ .describedAs("and the key half must answer from the same bound registry")
|
||||
+ .isTrue();
|
||||
+ }
|
||||
+
|
||||
+ // -------------------------------------------------------------------------------------------
|
||||
+ // 2. THE BOUNDARY IS EXACTLY ONE BLOCK. At H-2 and below, nothing changes: the head fallback
|
||||
+ // still answers, and on this empty head that answer is a refusal, exactly as before.
|
||||
+ // -------------------------------------------------------------------------------------------
|
||||
+
|
||||
+ @Test
|
||||
+ public void theBoundaryIsExactlyOneBlock() {
|
||||
+ final FalconSealSupport pqc = FalconSealSupport.instance();
|
||||
+ assertThat(pqc.addressForIndexAtHistoric(H - 2L, 0))
|
||||
+ .describedAs("H-2 is not the parent of any anchor-governed height: old behaviour, refusal")
|
||||
+ .isNull();
|
||||
+ assertThat(pqc.verifyAtHistoric(H - 2L, 0, MESSAGE, sealByIndexZero)).isFalse();
|
||||
+ assertThat(pqc.addressForIndexAtHistoric(0L, 0))
|
||||
+ .describedAs("and deep below, the same")
|
||||
+ .isNull();
|
||||
+ }
|
||||
+
|
||||
+ // -------------------------------------------------------------------------------------------
|
||||
+ // Helpers, borrowed from PqRegistryHeightRefusalTest.
|
||||
+ // -------------------------------------------------------------------------------------------
|
||||
+
|
||||
+ private PqRegistryHash.Schedule scheduleFromGenesis(final Map<Long, String> entries)
|
||||
+ throws Exception {
|
||||
+ final List<Long> heights = new ArrayList<>(entries.keySet());
|
||||
+ heights.sort(Long::compare);
|
||||
+ final StringBuilder sb = new StringBuilder("{\"config\":{\"pqRegistryHash\":[");
|
||||
+ for (int i = 0; i < heights.size(); i++) {
|
||||
+ if (i > 0) {
|
||||
+ sb.append(',');
|
||||
+ }
|
||||
+ final long b = heights.get(i);
|
||||
+ String h = entries.get(b);
|
||||
+ if (!h.startsWith("0x")) {
|
||||
+ h = "0x" + h;
|
||||
+ }
|
||||
+ sb.append("{\"block\":").append(b).append(",\"hash\":\"").append(h).append("\"}");
|
||||
+ }
|
||||
+ sb.append("]}}");
|
||||
+ final Path p =
|
||||
+ tmp.resolve("genesis-schedule-" + heights.size() + "-" + heights.get(0) + ".json");
|
||||
+ Files.writeString(p, sb.toString());
|
||||
+ return PqRegistryHash.loadScheduleFromGenesis(p);
|
||||
+ }
|
||||
+
|
||||
+ private static void resetFalconSingleton() throws Exception {
|
||||
+ final Field f = FalconSealSupport.class.getDeclaredField("instance");
|
||||
+ f.setAccessible(true);
|
||||
+ f.set(null, null);
|
||||
+ }
|
||||
+}
|
||||
diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryBindingTest.java
|
||||
new file mode 100755
|
||||
index 000000000..d902552cc
|
||||
|
||||
Loading…
Reference in New Issue
Block a user