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
|
**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
|
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.
|
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 =
|
final Optional<PqRegistryHash.ScheduleEntry> required =
|
||||||
PqRegistryHash.requiredHashAt(schedule, blockNumber);
|
PqRegistryHash.requiredHashAt(schedule, blockNumber);
|
||||||
if (required.isEmpty()) {
|
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(
|
return headRegistryKeyOrRefuse(
|
||||||
blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
|
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);
|
blockNumber, validatorIndex, "no schedule was ever loaded", historic);
|
||||||
}
|
}
|
||||||
if (PqRegistryHash.requiredHashAt(schedule, blockNumber).isEmpty()) {
|
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(
|
return headRegistryAddressOrRefuse(
|
||||||
blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
|
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>
|
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
|
Subject: [PATCH] Aere Network: post-quantum certificate anchor for QBFT
|
||||||
|
|
||||||
Puts a 32-byte digest of the validator certificate into vanityData, which is
|
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
|
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 certificate to the block hash; it does not make consensus
|
||||||
post-quantum.
|
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 +
|
.../org/hyperledger/besu/cli/BesuCommand.java | 24 +
|
||||||
.../cli/options/AerePqEmergencyOptions.java | 245 ++
|
.../cli/options/AerePqEmergencyOptions.java | 245 ++
|
||||||
@ -25,14 +37,14 @@ post-quantum.
|
|||||||
.../common/bft/BftBlockInterface.java | 22 +-
|
.../common/bft/BftBlockInterface.java | 22 +-
|
||||||
.../consensus/common/bft/BftExtraData.java | 50 +-
|
.../consensus/common/bft/BftExtraData.java | 50 +-
|
||||||
.../besu/consensus/common/bft/FalconSeal.java | 88 +
|
.../besu/consensus/common/bft/FalconSeal.java | 88 +
|
||||||
.../common/bft/FalconSealSupport.java | 3709 +++++++++++++++++
|
.../common/bft/FalconSealSupport.java | 3749 +++++++++++++++++
|
||||||
.../besu/consensus/common/bft/PqAnchor.java | 335 ++
|
.../besu/consensus/common/bft/PqAnchor.java | 335 ++
|
||||||
.../consensus/common/bft/PqAnchorConfig.java | 1320 ++++++
|
.../consensus/common/bft/PqAnchorConfig.java | 1320 ++++++
|
||||||
.../common/bft/PqAnchorNotReadyException.java | 132 +
|
.../common/bft/PqAnchorNotReadyException.java | 132 +
|
||||||
.../common/bft/PqAnchorSyncModeGuard.java | 130 +
|
.../common/bft/PqAnchorSyncModeGuard.java | 130 +
|
||||||
.../common/bft/PqAnchorThresholdGuard.java | 252 ++
|
.../common/bft/PqAnchorThresholdGuard.java | 252 ++
|
||||||
.../common/bft/PqRegistryBinding.java | 497 +++
|
.../common/bft/PqRegistryBinding.java | 497 +++
|
||||||
.../consensus/common/bft/PqRegistryHash.java | 2341 +++++++++++
|
.../consensus/common/bft/PqRegistryHash.java | 2341 ++++++++++
|
||||||
.../common/bft/PqRegistryHashTool.java | 482 +++
|
.../common/bft/PqRegistryHashTool.java | 482 +++
|
||||||
.../consensus/common/bft/PqSealCache.java | 351 ++
|
.../consensus/common/bft/PqSealCache.java | 351 ++
|
||||||
.../consensus/common/bft/PqSealStore.java | 384 ++
|
.../consensus/common/bft/PqSealStore.java | 384 ++
|
||||||
@ -40,7 +52,7 @@ post-quantum.
|
|||||||
.../blockcreation/BftBlockCreatorFactory.java | 25 +-
|
.../blockcreation/BftBlockCreatorFactory.java | 25 +-
|
||||||
.../bft/blockcreation/PqAnchorProducer.java | 347 ++
|
.../bft/blockcreation/PqAnchorProducer.java | 347 ++
|
||||||
.../common/bft/tools/PqRegistryHashTool.java | 231 +
|
.../common/bft/tools/PqRegistryHashTool.java | 231 +
|
||||||
.../common/bft/FalconAttachIntervalTest.java | 232 ++
|
.../common/bft/FalconAttachIntervalTest.java | 232 +
|
||||||
.../common/bft/PqAnchorConfigTest.java | 691 +++
|
.../common/bft/PqAnchorConfigTest.java | 691 +++
|
||||||
.../bft/PqAnchorEmergencyConfigTest.java | 120 +
|
.../bft/PqAnchorEmergencyConfigTest.java | 120 +
|
||||||
.../common/bft/PqAnchorIntervalTest.java | 186 +
|
.../common/bft/PqAnchorIntervalTest.java | 186 +
|
||||||
@ -56,13 +68,14 @@ post-quantum.
|
|||||||
.../bft/PqForkThresholdReachabilityTest.java | 355 ++
|
.../bft/PqForkThresholdReachabilityTest.java | 355 ++
|
||||||
.../bft/PqForkValidatorSetChangeTest.java | 427 ++
|
.../bft/PqForkValidatorSetChangeTest.java | 427 ++
|
||||||
.../common/bft/PqInertBinaryTest.java | 475 +++
|
.../common/bft/PqInertBinaryTest.java | 475 +++
|
||||||
|
.../bft/PqParentHeightAlignmentTest.java | 229 +
|
||||||
.../common/bft/PqRegistryBindingTest.java | 595 +++
|
.../common/bft/PqRegistryBindingTest.java | 595 +++
|
||||||
.../bft/PqRegistryHeightRefusalTest.java | 328 ++
|
.../bft/PqRegistryHeightRefusalTest.java | 328 ++
|
||||||
.../common/bft/PqRegistryRotationTest.java | 476 +++
|
.../common/bft/PqRegistryRotationTest.java | 476 +++
|
||||||
.../common/bft/PqSealPersistenceTest.java | 621 +++
|
.../common/bft/PqSealPersistenceTest.java | 621 +++
|
||||||
.../common/bft/PqSignedHeightTest.java | 403 ++
|
.../common/bft/PqSignedHeightTest.java | 403 ++
|
||||||
.../common/bft/PqStartupHistoryTest.java | 325 ++
|
.../common/bft/PqStartupHistoryTest.java | 325 ++
|
||||||
.../consensus/common/bft/PqV2Fixture.java | 233 ++
|
.../consensus/common/bft/PqV2Fixture.java | 233 +
|
||||||
.../qbft/core/messagewrappers/Commit.java | 18 +
|
.../qbft/core/messagewrappers/Commit.java | 18 +
|
||||||
.../core/network/QbftMessageTransmitter.java | 25 +-
|
.../core/network/QbftMessageTransmitter.java | 25 +-
|
||||||
.../qbft/core/payload/CommitPayload.java | 147 +-
|
.../qbft/core/payload/CommitPayload.java | 147 +-
|
||||||
@ -91,7 +104,7 @@ post-quantum.
|
|||||||
.../PqEmergencyShoutRuleTest.java | 78 +
|
.../PqEmergencyShoutRuleTest.java | 78 +
|
||||||
.../PqForkGateFeedTest.java | 189 +
|
.../PqForkGateFeedTest.java | 189 +
|
||||||
.../eth/sync/DownloadHeadersStep.java | 103 +-
|
.../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/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 app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
|
||||||
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
|
create mode 100755 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/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/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/PqForkValidatorSetChangeTest.java
|
||||||
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqInertBinaryTest.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/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/PqRegistryHeightRefusalTest.java
|
||||||
create mode 100755 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.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
|
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
|
new file mode 100755
|
||||||
index 000000000..b042e1589
|
index 000000000..af694dcf9
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/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
|
||||||
@@ -0,0 +1,3709 @@
|
@@ -0,0 +1,3749 @@
|
||||||
+/*
|
+/*
|
||||||
+ * Copyright contributors to Besu.
|
+ * Copyright contributors to Besu.
|
||||||
+ *
|
+ *
|
||||||
@ -4683,6 +4697,25 @@ index 000000000..b042e1589
|
|||||||
+ final Optional<PqRegistryHash.ScheduleEntry> required =
|
+ final Optional<PqRegistryHash.ScheduleEntry> required =
|
||||||
+ PqRegistryHash.requiredHashAt(schedule, blockNumber);
|
+ PqRegistryHash.requiredHashAt(schedule, blockNumber);
|
||||||
+ if (required.isEmpty()) {
|
+ 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(
|
+ return headRegistryKeyOrRefuse(
|
||||||
+ blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
|
+ 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);
|
+ blockNumber, validatorIndex, "no schedule was ever loaded", historic);
|
||||||
+ }
|
+ }
|
||||||
+ if (PqRegistryHash.requiredHashAt(schedule, blockNumber).isEmpty()) {
|
+ 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(
|
+ return headRegistryAddressOrRefuse(
|
||||||
+ blockNumber, validatorIndex, "the schedule binds no registry at this height", historic);
|
+ 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
|
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
|
new file mode 100755
|
||||||
index 000000000..d902552cc
|
index 000000000..d902552cc
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user