Anchor corpus at the live overlay: hybrid v2 certificate (Falcon-512 + SLH-DSA-SHA2-128s), in-tree SLH-DSA engine on the JDK digest (Bouncy Castle licence alongside), seal-store fix (D-339), anchor interval schedule; docs and SPEC 1.2.0 state the arming heights measured on the fleet on 2026-09-05
This commit is contained in:
parent
971754aad9
commit
32ca7453e0
16
README.md
16
README.md
@ -16,12 +16,16 @@ consensus post-quantum, and nothing in the precompile patches touches consensus.
|
|||||||
|
|
||||||
Consensus on chain 2800 is **hybrid post-quantum in a precise and limited sense**, by a separate
|
Consensus on chain 2800 is **hybrid post-quantum in a precise and limited sense**, by a separate
|
||||||
mechanism: the QBFT header anchor in `anchor/` and `patches/0003`. Since 2026-08-14 every
|
mechanism: the QBFT header anchor in `anchor/` and `patches/0003`. Since 2026-08-14 every
|
||||||
32nd block (an anchor block) must carry, under its block hash, a certificate of at least three
|
32nd block (an anchor block) must carry, under its block hash, a certificate of valid post-quantum
|
||||||
valid Falcon-512 validator seals (f+1 of nine; eight or nine are carried in practice), and a node
|
validator seals, and a node does not finalize an anchor block without it: at least three Falcon-512
|
||||||
does not finalize an anchor block without it. Raised at block 14,961,456 (August 21, 2026): the
|
seals at first (f+1 of nine), **six of nine, a full 2f+1 quorum, since block 14,961,456** (August 21,
|
||||||
enforced minimum is now **six of nine, a full 2f+1 quorum**; the schedule step is
|
2026), and since block 17,047,600 (2026-09-04) a **hybrid** certificate of Falcon-512 AND
|
||||||
`aere.pq.anchorMinSeals=13014000:0,13034000:3,14961456:6`, uniform on all nine validators. Classical secp256k1 ECDSA QBFT still finalizes every
|
SLH-DSA-SHA2-128s seals, six of nine of each scheme. From block 17,225,968 (schedule set on all nine
|
||||||
block; validator identity and QBFT message transport are classical. Correction 2026-08-19: the
|
validators on 2026-09-05) the anchor falls on every 128th block instead of every 32nd. Classical
|
||||||
|
secp256k1 ECDSA QBFT still finalizes every block and validator identity is classical; the QBFT
|
||||||
|
messages themselves carry post-quantum seals of their authors on the live fleet (emission since
|
||||||
|
blocks 16,620,000 to 16,690,000), with refusal of unsealed messages armed for blocks 17,250,000 to
|
||||||
|
17,700,000 (set 2026-09-05; see `anchor/README.md`, the arming table). Correction 2026-08-19: the
|
||||||
per-block Falcon quorum rule armed at block 14,050,000 (`aere.falcon.forkBlock`) is, in the shipped
|
per-block Falcon quorum rule armed at block 14,050,000 (`aere.falcon.forkBlock`) is, in the shipped
|
||||||
code, retired in favour of the anchor rules from block 13,014,000 and changed no enforcement, so
|
code, retired in favour of the anchor rules from block 13,014,000 and changed no enforcement, so
|
||||||
the earlier statement that from that height every block required a 2f+1 Falcon quorum to finalize
|
the earlier statement that from that height every block required a 2f+1 Falcon quorum to finalize
|
||||||
|
|||||||
48
SPEC.md
48
SPEC.md
@ -1,6 +1,6 @@
|
|||||||
# Aere Network chain 2800: full protocol specification
|
# Aere Network chain 2800: full protocol specification
|
||||||
|
|
||||||
Version: 1.1.0, written 2026-08-25, amended 2026-09-02 (section 2.6, D8, G11). Scope: everything a third party needs to implement a client
|
Version: 1.2.0, written 2026-08-25, amended 2026-09-02 (section 2.6, D8, G11) and 2026-09-05 (sections 2.6 and 3.1: the message-layer arming heights, the v2 hybrid certificate, the anchor interval schedule). Scope: everything a third party needs to implement a client
|
||||||
that follows chain 2800 from block 0 to the live head and validates what the network validates,
|
that follows chain 2800 from block 0 to the live head and validates what the network validates,
|
||||||
without reading Aere source code. It specifies the follower (validation) protocol completely; it
|
without reading Aere source code. It specifies the follower (validation) protocol completely; it
|
||||||
describes seal production only where a follower must understand it to validate.
|
describes seal production only where a follower must understand it to validate.
|
||||||
@ -264,7 +264,7 @@ factory and grepping `consensus/`; recorded in `RUN-A-NODE.md`, "Which Besu", an
|
|||||||
over the block hash, which commits to it. Section 5 states what this means for a client that DOES
|
over the block hash, which commits to it. Section 5 states what this means for a client that DOES
|
||||||
validate base fees.
|
validate base fees.
|
||||||
|
|
||||||
### 2.6 Consensus messages under post-quantum seal (emission live since 2026-09-02; enforcement not yet armed)
|
### 2.6 Consensus messages under post-quantum seal (emission live since 2026-09-02; enforcement armed by height on 2026-09-05)
|
||||||
|
|
||||||
Since 28-31 August 2026 every one of the four QBFT message types (PROPOSAL, PREPARE, COMMIT,
|
Since 28-31 August 2026 every one of the four QBFT message types (PROPOSAL, PREPARE, COMMIT,
|
||||||
ROUND-CHANGE) can carry a Falcon-512 seal of its author, and since 2 September 2026 all nine
|
ROUND-CHANGE) can carry a Falcon-512 seal of its author, and since 2 September 2026 all nine
|
||||||
@ -273,9 +273,15 @@ validators of chain 2800 EMIT such seals on every message they send (source:
|
|||||||
`ProposalPayload.java`, `PreparePayload.java`, `CommitPayload.java`, `RoundChangePayload.java`;
|
`ProposalPayload.java`, `PreparePayload.java`, `CommitPayload.java`, `RoundChangePayload.java`;
|
||||||
fleet configuration measured 2026-09-02). This section specifies the wire format, the signed
|
fleet configuration measured 2026-09-02). This section specifies the wire format, the signed
|
||||||
pre-images, the emission gates and the enforcement rules exactly as the shipped client implements
|
pre-images, the emission gates and the enforcement rules exactly as the shipped client implements
|
||||||
them. **Enforcement is a per-node height switch and is NOT active on chain 2800 at publication:**
|
them. **Enforcement is a per-node height switch.** On chain 2800 the four heights were set on all
|
||||||
a message without a seal, or with an invalid seal, still counts as a vote everywhere on 2800. On
|
nine validators on 2026-09-05 [MEASURED 2026-09-05, `docker inspect` of every validator]:
|
||||||
the public testnet 28001 all four enforcements are active (source: `testnet-public/CITESTE-MA.md`).
|
`aere.pq.commitPq.forkBlock=17250000`, `aere.pq.proposalPq.forkBlock=17400000`,
|
||||||
|
`aere.pq.roundChangePq.forkBlock=17550000`, `aere.pq.preparePq.forkBlock=17700000`. Below each
|
||||||
|
height a message of that type without a seal, or with an invalid seal, still counts as a vote; from
|
||||||
|
it, it does not. Emission heights on 2800: `proposalPq.attachBlock=16620000`,
|
||||||
|
`roundChangePq.attachBlock=16660000`, `preparePq.attachBlock=16690000` (COMMIT seals are emitted
|
||||||
|
from the anchor). On the public testnet 28001 all four enforcements have been active since
|
||||||
|
2 September 2026 (source: `testnet-public/CITESTE-MA.md`).
|
||||||
|
|
||||||
**Wire format.** Each payload keeps its upstream RLP list and appends, as its LAST element, an
|
**Wire format.** Each payload keeps its upstream RLP list and appends, as its LAST element, an
|
||||||
optional two-element list `[validatorIndex, signature]` (`validatorIndex` an integer index into the
|
optional two-element list `[validatorIndex, signature]` (`validatorIndex` an integer index into the
|
||||||
@ -285,8 +291,10 @@ and `readFrom` refuses any encoding that does not reproduce the received bytes,
|
|||||||
a seal and the same message without one never share a hash (source: `PreparePayload.writeTo`,
|
a seal and the same message without one never share a hash (source: `PreparePayload.writeTo`,
|
||||||
comment "This method DEFINES the canonical encoding"). A COMMIT may additionally carry a list of
|
comment "This method DEFINES the canonical encoding"). A COMMIT may additionally carry a list of
|
||||||
non-Falcon "extra" seals for the hybrid certificate (`[schemeWireId, index, signature]`), REQUIRED
|
non-Falcon "extra" seals for the hybrid certificate (`[schemeWireId, index, signature]`), REQUIRED
|
||||||
to be accompanied by a Falcon seal; no extra seals exist on the fleet today (source:
|
to be accompanied by a Falcon seal. Since block 17,047,568 (`aere.pq.hybrid.attachBlock`) the
|
||||||
`CommitPayload.java`, constructor javadoc "empty for every commit on the fleet today").
|
COMMIT of an anchor's PARENT carries one SLH-DSA-SHA2-128s extra seal per validator; other commits
|
||||||
|
carry none, because that seal costs seconds and is used only in the next anchor's certificate
|
||||||
|
[MEASURED 2026-09-05] (source: `CommitPayload.java`; `HybridSealProducer.java`).
|
||||||
|
|
||||||
**Signed pre-images: one domain per message type.** Every seal signs
|
**Signed pre-images: one domain per message type.** Every seal signs
|
||||||
`keccak256(RLP[DOMAIN, chainId, height, ...])` with an ASCII domain string that names the message
|
`keccak256(RLP[DOMAIN, chainId, height, ...])` with an ASCII domain string that names the message
|
||||||
@ -364,9 +372,19 @@ section 3.1.
|
|||||||
|
|
||||||
### 3.1 The claim, stated exactly
|
### 3.1 The claim, stated exactly
|
||||||
|
|
||||||
From block 13,014,000, every 32nd header of chain 2800 (an "anchor block") carries a certificate
|
From block 13,014,000, every 32nd header of chain 2800 (an "anchor block"), and from block
|
||||||
of validator Falcon-512 seals over its parent, and the anchor block's keccak block hash covers a
|
17,225,968 every 128th (`aere.pq.anchorIntervalSchedule=17225968:128`, set on every node on
|
||||||
32-byte commitment to that certificate. The enforced minimum number of valid seals is a height
|
2026-09-05), carries a certificate of validator seals over its parent, and the anchor block's keccak
|
||||||
|
block hash covers a 32-byte commitment to that certificate. Until block 17,047,600 the certificate is
|
||||||
|
v1, a list of `[index, falconSignature]` pairs under the domain `AERE-PQ-ANCHOR-1`; from block
|
||||||
|
17,047,600 (`aere.pq.anchorV2Block`) it is v2, `RLP[2, [[scheme, index, signature], ...]]` under the
|
||||||
|
domain `AERE-PQ-ANCHOR-2`, digest `keccak256(RLP[domain, chainId, parentNumber, parentHash,
|
||||||
|
BYTES(rlp(certificate))])`, with scheme 1 = Falcon-512 and 2 = SLH-DSA-SHA2-128s; the scheme
|
||||||
|
schedule `aere.pq.schemeSchedule=17047568:falcon-512+slh-dsa-sha2-128s` names which schemes are
|
||||||
|
required, the minimum applies to each scheme separately, and every SLH-DSA seal must sit at an index
|
||||||
|
that also carries a valid Falcon seal [MEASURED 2026-09-05, 400 consecutive anchors] (source:
|
||||||
|
`anchor/consensus/common/.../PqAnchorV2.java`, `PqSchemeSchedule.java`; the public verifier
|
||||||
|
`tools/verify-anchor.mjs`). The enforced minimum number of valid seals is a height
|
||||||
schedule: 0 from 13,014,000 (warm-up), 3 from 13,034,000 (f+1 of nine; enforced on the fleet since
|
schedule: 0 from 13,014,000 (warm-up), 3 from 13,034,000 (f+1 of nine; enforced on the fleet since
|
||||||
2026-08-14), and **6 of 9 from block 14,961,456** (a full 2f+1 quorum, the same count QBFT itself
|
2026-08-14), and **6 of 9 from block 14,961,456** (a full 2f+1 quorum, the same count QBFT itself
|
||||||
requires) (source: `anchor/README.md`, "Scope boundary"; `RUN-A-NODE.md`, the anchor section).
|
requires) (source: `anchor/README.md`, "Scope boundary"; `RUN-A-NODE.md`, the anchor section).
|
||||||
@ -413,6 +431,10 @@ passes"):
|
|||||||
-Daere.pq.chainId=2800
|
-Daere.pq.chainId=2800
|
||||||
-Daere.pq.anchorBlock=13014000
|
-Daere.pq.anchorBlock=13014000
|
||||||
-Daere.pq.anchorInterval=32
|
-Daere.pq.anchorInterval=32
|
||||||
|
-Daere.pq.anchorIntervalSchedule=17225968:128
|
||||||
|
-Daere.pq.anchorV2Block=17047600
|
||||||
|
-Daere.pq.hybrid.attachBlock=17047568
|
||||||
|
-Daere.pq.schemeSchedule=17047568:falcon-512+slh-dsa-sha2-128s
|
||||||
-Daere.pq.anchor.maxSeals=9
|
-Daere.pq.anchor.maxSeals=9
|
||||||
-Daere.pq.anchorMinSeals=13014000:0,13034000:3,14961456:6
|
-Daere.pq.anchorMinSeals=13014000:0,13034000:3,14961456:6
|
||||||
-Daere.pq.anchor.minSealsCeiling=1
|
-Daere.pq.anchor.minSealsCeiling=1
|
||||||
@ -437,6 +459,12 @@ Semantics of each parameter (source: `PqAnchorConfig.java`):
|
|||||||
- `aere.pq.anchorInterval` (32): anchor heights are counted FROM H:
|
- `aere.pq.anchorInterval` (32): anchor heights are counted FROM H:
|
||||||
`(n - 13014000) % 32 == 0`, equivalently `n mod 32 == 16`. H itself is an anchor height. Between
|
`(n - 13014000) % 32 == 0`, equivalently `n mod 32 == 16`. H itself is an anchor height. Between
|
||||||
anchor heights the rules judge nothing.
|
anchor heights the rules judge nothing.
|
||||||
|
- `aere.pq.anchorIntervalSchedule` (`h:interval,...`): from `h` the interval changes; `h` must itself
|
||||||
|
be on the previous grid and the new interval a multiple of the old one, so every new anchor height is
|
||||||
|
also an old one. On 2800: `17225968:128`, so from that block `(n - 13014000) % 128 == 0`. A node
|
||||||
|
without the schedule keeps judging the old grid and rejects the first old-grid height that carries
|
||||||
|
no certificate; every node that judges anchors must carry the same schedule [MEASURED 2026-09-05 on
|
||||||
|
the public testnet, schedule `312000:128`].
|
||||||
- `aere.pq.anchorMinSeals`: `height:K` steps, floor semantics via `floorEntry(height)`; a step must
|
- `aere.pq.anchorMinSeals`: `height:K` steps, floor semantics via `floorEntry(height)`; a step must
|
||||||
exist exactly at H; steps below H refuse.
|
exist exactly at H; steps below H refuse.
|
||||||
- `aere.pq.anchor.minSealsCeiling`: effective K is `min(scheduled, ceiling)`. The ceiling can only
|
- `aere.pq.anchor.minSealsCeiling`: effective K is `min(scheduled, ceiling)`. The ceiling can only
|
||||||
|
|||||||
@ -5,20 +5,22 @@ computed them, and the same 276 files once produced two different aggregates her
|
|||||||
|
|
||||||
329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
|
329e4e2f93143cc8af092f0a951fa23619c12c4ec1793b053654f8e26e088da0 app/src/main/java/org/hyperledger/besu/cli/BesuCommand.java
|
||||||
3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
|
3f289d74bb1878280a99742320d7a27c19146a2f19cdf6a0857f2aad4361ad5e app/src/main/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptions.java
|
||||||
51764581814253cf76fa631e2d6557581a7bf9ae585a701009e37b80345f8867 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
|
05765f25da67f9b14d490121e73536a41c107edcc841c7303afaae93061ebf39 app/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java
|
||||||
129493150c0b13e8020bdf50c0c1ae07268d4f73a69a821b303bf4894451fa5e app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
|
129493150c0b13e8020bdf50c0c1ae07268d4f73a69a821b303bf4894451fa5e app/src/test/java/org/hyperledger/besu/cli/options/AerePqEmergencyOptionsTest.java
|
||||||
728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java
|
728c29c299edf6011f2c2798cbe930a043112e0e06b924e7edb54284ca44a758 config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java
|
||||||
a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle
|
a3b93f2602e9755d91358c9b3f473235f4d40ae23d912f797cdb03ed7e1348f9 consensus/common/build.gradle
|
||||||
657b2c652c7995976acab3feca7fec5e15acc66cdfe1d5cc23ced6813db17a5f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java
|
68d635954ef33448a661256f50642eeeff2f7bb5874a54ffc2e64193c83fe391 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelay.java
|
||||||
e32a03de9f1452bd7444a33b084516ae3399d7a5ae7430088714f5656d3a22ea consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java
|
8fa317b1ba1d887f0623610f8721d728d954cc800d3b391729ed06f64c8141a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftBlockInterface.java
|
||||||
|
2e84bb48611d3da7c46bcd4018ae746cbf785dfcb16f495674a0f6d542e71e8e consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BftExtraData.java
|
||||||
|
adcc6c05e998f279fc6b863ead155f78b936a969985de40e1dda5bab641e73cf consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java
|
||||||
3ab425b5b5f7d7c2199065a3905623d5c1c4c5922d892c6c4275415b0c69a2b4 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
|
3ab425b5b5f7d7c2199065a3905623d5c1c4c5922d892c6c4275415b0c69a2b4 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSeal.java
|
||||||
db1e80115ce59c8281fbf046cb37e153a49fe017c1675afd200e8125d3f6b538 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java
|
db1e80115ce59c8281fbf046cb37e153a49fe017c1675afd200e8125d3f6b538 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealScheme.java
|
||||||
789db234a90f118cdf881653ee2bb7ad4a229a685315b04822147ffffe6a3c13 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
|
789db234a90f118cdf881653ee2bb7ad4a229a685315b04822147ffffe6a3c13 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/FalconSealSupport.java
|
||||||
e8c8111a343cc993b950ac4e5b3992f16b8ee3d34c8827f00dd80d017884335b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java
|
e8c8111a343cc993b950ac4e5b3992f16b8ee3d34c8827f00dd80d017884335b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducer.java
|
||||||
03043d2360c35de3b6f27d029b7ee83cb6c964f4b827a504799607b8bfe1cdcc consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupport.java
|
be99cd76f4b80a0c83e0a18801249c7972a9e93c54bbad6158d1155753a7ad6a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupport.java
|
||||||
65e33e639c58adb30045e88c770460757305acab31ac884e4c6b36f1c410ef12 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistry.java
|
681d5ca2ea929d81f92664341d2651160d50621efb5194808627fb61834c292b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistry.java
|
||||||
ecd0e865aa48e461618048c13c46d9b54095dfa2e14180750f49b90faf49302a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
|
ecd0e865aa48e461618048c13c46d9b54095dfa2e14180750f49b90faf49302a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchor.java
|
||||||
852113d420f2c6b82945423a18591c3b2bd1018febe1a52e8b4e42a996c85114 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java
|
1c3d90324c6f5116194bae0539680fdc1f528ba4c893c5e2b44a592aea6b3abd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfig.java
|
||||||
c6285ca43331781e1d350a07d58256d02b8b3a74b96b6469c0dca33ecfb6c8cd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java
|
c6285ca43331781e1d350a07d58256d02b8b3a74b96b6469c0dca33ecfb6c8cd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapse.java
|
||||||
f27952a25bad02d2939a356f1bac8800ff8fb08023c6bdfb1c6d0e51b5a206da consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java
|
f27952a25bad02d2939a356f1bac8800ff8fb08023c6bdfb1c6d0e51b5a206da consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorNotReadyException.java
|
||||||
81ddbf2b40f79bf0e382c507235f07ad74937ffd455e387c265880bdcf4e2b5c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java
|
81ddbf2b40f79bf0e382c507235f07ad74937ffd455e387c265880bdcf4e2b5c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSyncModeGuard.java
|
||||||
@ -28,22 +30,44 @@ e16b2c654fe21893c7972dac71f3ffafd1e4e8efbecdb6f95051820e36fbd48e consensus/comm
|
|||||||
f67a205036550f7d0f1bfed2abe7b89abd73e80614adff8d696b33c580269a6f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHash.java
|
f67a205036550f7d0f1bfed2abe7b89abd73e80614adff8d696b33c580269a6f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHash.java
|
||||||
3fbd850e00e487c40c61310d894d6e1f765e9098e5c55438f9b06ab8fb92d45b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
|
3fbd850e00e487c40c61310d894d6e1f765e9098e5c55438f9b06ab8fb92d45b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHashTool.java
|
||||||
e85f4be696700184f9700cc531b4f3df313e9fb8cff8e2bc39d24241a769addb consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSchemeSchedule.java
|
e85f4be696700184f9700cc531b4f3df313e9fb8cff8e2bc39d24241a769addb consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSchemeSchedule.java
|
||||||
d2e8b34f29d0dd83a66b78b53ce430404834cdf9ba334abeb72481d5d681aeb9 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
|
c0c93a096f4eceab6bf92cc3618ce7cee1e54d2d20b6ac781850a390ec6e514f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealCache.java
|
||||||
e8f7a8c4d8f1626d8efd167e5096de14ca17d40af58577737c5ef6a769dcc4d5 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java
|
73abbaddcec1533a7a758f1c4a26ae68a83ddbb96fd98ad147e74d7386bbbf3f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSealStore.java
|
||||||
a235b3ec8c69665c525d7c74a084d589904879f5cd33cb87e1d1f38eabba8a15 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
|
a235b3ec8c69665c525d7c74a084d589904879f5cd33cb87e1d1f38eabba8a15 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/PqSignerRegistry.java
|
||||||
addba40c0d931a3ecfa3b2f0179311dacaa604884e3ebb1d79f958bd913f72e2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SchemeSeal.java
|
addba40c0d931a3ecfa3b2f0179311dacaa604884e3ebb1d79f958bd913f72e2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SchemeSeal.java
|
||||||
285f6a7c1188a387fb1ac21f5ab809595ef48d936b40aced94047525e6def0a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java
|
285f6a7c1188a387fb1ac21f5ab809595ef48d936b40aced94047525e6def0a6 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealScheme.java
|
||||||
352ffd303d2fcdc0d09ab943cebb2cd22b9c6329e129eb522f51fed8357f877d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealSchemes.java
|
3b57840dc103063b3797697feb78b90af93ca123587e9278cfb43b84edbd995a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SealSchemes.java
|
||||||
1ef188292858db2a6b70c074a2477223bf96e2762a3711c8a993404eb2db2cfa consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SlhDsaSealScheme.java
|
444961503d515cb9f045729577e190941e59baa4bd90438a2da05f41fed7750a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/SlhDsaSealScheme.java
|
||||||
b6c9ecbf3cd2ee73111984cd04a89c32ee56a1cc699a197b65ef7c10001b8cbc consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java
|
b6c9ecbf3cd2ee73111984cd04a89c32ee56a1cc699a197b65ef7c10001b8cbc consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/BftBlockCreatorFactory.java
|
||||||
5c8861eba1ea697d8deb88139d92c6ea6636b29d76c881b13e7f6c841ba2324f consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java
|
15ee94346cd803530cf9362b74c645aad0986c6d26a5386f9f38ee4e92a66b7b consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/blockcreation/PqAnchorProducer.java
|
||||||
|
baa7f138046c501c5baecae3615d54ed0bbd6fea47d7f84a3d71eaa5698d182d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/ADRS.java
|
||||||
|
7c464b927fcb39ab017c543ca42bcd076b1dc69573c7ddefda1e685ebf9eb9ba consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/Fors.java
|
||||||
|
ddb17d7c6e1b762a9e8bcc924a17d42218cb5ec16528d80170bf2ff07e40aba2 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/HT.java
|
||||||
|
daf4c242682df6365aa7459990a9ba7b5f97e9a06cfff95c96afe1eadc061647 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/IndexedDigest.java
|
||||||
|
ddd60bf21ee7207b5b7d72fda5cd8bd9f9515536744379aa7d1bb7e3cdc9abdb consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/JdkDigest.java
|
||||||
|
108a2d252eea02e005889942966d2dcf845e1d01fc5e7bfdf85b58e1ac0697cd consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/NodeEntry.java
|
||||||
|
91ff4c4bc681bde06e72188e28177170645ca72cb72a08b08c3cf2283376a933 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/PK.java
|
||||||
|
ef1fcba8ac1d98f6bcb44616b33551cc37c21fa4fd0e5adc967bd4960c6bc184 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG.java
|
||||||
|
9b9b7b171e816d6b142978ada7ea4b4491dbad025d26f431d6a6fc03130b047a consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_FORS.java
|
||||||
|
21371b535b9818ba0bb4b094837b11b00dc3132187a7b67f1b11196ab386f1f5 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SIG_XMSS.java
|
||||||
|
07ed38417f5f4a4c56631c82651e70708ab47b8b6f017096ea281f0436180671 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SK.java
|
||||||
|
8107ac08eabfee9016ecea9cdb53b78409f322c49706224def9c05501b297985 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngine.java
|
||||||
|
efa6edc2f6cdf5e67074ef793fb15fff1bf41f5030476b7f8de991ada54b15eb consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAEngineProvider.java
|
||||||
|
fa89107a6ff29b8d55d66949dd810cad36851a8ac847a0f7f33a843551f0e359 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyGenerationParameters.java
|
||||||
|
48e4e7448b8076376bcd5720e20dd36d07b9d7a953fdf992691d54fa20f1709d consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyPairGenerator.java
|
||||||
|
14411ba911129fb56f95c62a26aa00de8b2fa7de5994563ec10d89ada95cc697 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAKeyParameters.java
|
||||||
|
c72f197a0f8967aec5e08d1c4bb30030b9dc16c27711567ca40ad0cbe845c989 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAParameters.java
|
||||||
|
3c39912f4a7f1d56ffbaf1b249ecb6726cbf98180e93c6a924fe3318552caaf0 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPrivateKeyParameters.java
|
||||||
|
3a18d7af3c85ab6a9239ff52be7524d141ed7e9e6cf84d34e8e4957434596b4c consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSAPublicKeyParameters.java
|
||||||
|
636399666ed9490b29a4cb85d2b3cddf2ef4e8f8fa9a8ecf2315a8abf3e2e6de consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/SLHDSASigner.java
|
||||||
|
bd6833075646ffa2f4fca2761e3d68ccbc37f7c5bb209ae2e40f8cfd469dce14 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/WotsPlus.java
|
||||||
6ace00e18914a1558563e689b7427654b5e6226a31447bef1d0416d993f78840 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java
|
6ace00e18914a1558563e689b7427654b5e6226a31447bef1d0416d993f78840 consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/tools/PqRegistryHashTool.java
|
||||||
|
cf0dcc008ce2d040590329088abdcbfc0829bf529445422deee8022d4856e08a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/AereAnchorProposalDelayTest.java
|
||||||
acd5c1e7f49dbbf444c19c91386bbd03b9298d952d2d13721793670103e71ee9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D177NeutralNamesTest.java
|
acd5c1e7f49dbbf444c19c91386bbd03b9298d952d2d13721793670103e71ee9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/D177NeutralNamesTest.java
|
||||||
c574ec22e642ac464b1728d27e4bb26202448a45c044f08935cc0a0efa0297d9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FalconAttachIntervalTest.java
|
c574ec22e642ac464b1728d27e4bb26202448a45c044f08935cc0a0efa0297d9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/FalconAttachIntervalTest.java
|
||||||
0eb20506f851c1510bcaf659db8c5b0d384cdc60ed06192a03a665f0ee2b3912 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducerTest.java
|
0eb20506f851c1510bcaf659db8c5b0d384cdc60ed06192a03a665f0ee2b3912 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealProducerTest.java
|
||||||
186c4c92199400783ee3424231818dd440b0da41aeb3d8bd643cd13759998250 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupportTest.java
|
186c4c92199400783ee3424231818dd440b0da41aeb3d8bd643cd13759998250 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSealSupportTest.java
|
||||||
834861639fd119c1653e6a3977ffbe4c2d4dce63a5741f1fe5a087643b86ed74 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java
|
834861639fd119c1653e6a3977ffbe4c2d4dce63a5741f1fe5a087643b86ed74 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/HybridSignerRegistryTest.java
|
||||||
220fceda0f5292054e322bb7df2d5258905a8a7b6f9febd802789d3946a0c01c consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java
|
2920f18dae0eb7ccead43d80202580b116e28c9fae5bb75d54917ddb99b9c2e9 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorConfigTest.java
|
||||||
6e2ae09f62765d6e558fdf56a124c890681c2b2c3d410bf8aa497820cd76c3a5 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java
|
6e2ae09f62765d6e558fdf56a124c890681c2b2c3d410bf8aa497820cd76c3a5 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorEmergencyConfigTest.java
|
||||||
7470d72271dd4f9de96094b3d1c6dc4b5b5ea62e46a058d4e154e3742d3ccef1 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java
|
7470d72271dd4f9de96094b3d1c6dc4b5b5ea62e46a058d4e154e3742d3ccef1 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorIntervalTest.java
|
||||||
e2487ff508ffb51bb61ca19531f4c4e68c5981611fd9fec5c92a2965f0bb8f47 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java
|
e2487ff508ffb51bb61ca19531f4c4e68c5981611fd9fec5c92a2965f0bb8f47 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorLapseTest.java
|
||||||
@ -53,6 +77,7 @@ fe84c239e3504f55e8d950e53b844641cc1dd7a7c864dd8f58db3d2f50c12b9d consensus/comm
|
|||||||
23daeb4888c8de8337518a27ef60e42573ebafaf340075e8ce9236f0d8d580f6 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java
|
23daeb4888c8de8337518a27ef60e42573ebafaf340075e8ce9236f0d8d580f6 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorSealCapTest.java
|
||||||
09c41c77408acc4711e4c46175485bf41babff0d0a8ec7481670fddfd28967b0 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java
|
09c41c77408acc4711e4c46175485bf41babff0d0a8ec7481670fddfd28967b0 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorTest.java
|
||||||
6ff9586cf0d544590585ad27227f7775f1dd86c61ccdd475321180d4e53f47d2 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuardTest.java
|
6ff9586cf0d544590585ad27227f7775f1dd86c61ccdd475321180d4e53f47d2 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorThresholdGuardTest.java
|
||||||
|
39656762e621e76b33ced524314e9692cfa875d7e8db8dbb22aaa6b7f9ad5c14 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2ProducerTest.java
|
||||||
72e58681267664a864dba5a371ffbc6f296b524c2dee248893b17c12da113f00 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2Test.java
|
72e58681267664a864dba5a371ffbc6f296b524c2dee248893b17c12da113f00 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqAnchorV2Test.java
|
||||||
49c95a24ce4890fe79b9167c196ba8da460e03bdddfee5652944e130fed8ee78 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqArmingGateTest.java
|
49c95a24ce4890fe79b9167c196ba8da460e03bdddfee5652944e130fed8ee78 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqArmingGateTest.java
|
||||||
74e2629acdcf9242c32340679914fa40f50d0a9479c83018b9b7a0733397a843 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqCallerIntentTest.java
|
74e2629acdcf9242c32340679914fa40f50d0a9479c83018b9b7a0733397a843 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqCallerIntentTest.java
|
||||||
@ -66,7 +91,7 @@ f260d284f936307ac4c31142cbea0359ded1197880b8a960ac27dd7beb5c3b53 consensus/comm
|
|||||||
97281811ae1fe6fae8001d0d64e11d4779be153547ab24c779a54aba3e211c88 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHeightRefusalTest.java
|
97281811ae1fe6fae8001d0d64e11d4779be153547ab24c779a54aba3e211c88 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryHeightRefusalTest.java
|
||||||
7ec10b4fa5ac09e4d980f5d180c75eecc892d95c2f81d97c6140e64ada34654d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
|
7ec10b4fa5ac09e4d980f5d180c75eecc892d95c2f81d97c6140e64ada34654d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqRegistryRotationTest.java
|
||||||
36cd369e8386cac85501f7d8a56f9604ec39e8bd89a8d754cabe85e4fb6b8e9a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
|
36cd369e8386cac85501f7d8a56f9604ec39e8bd89a8d754cabe85e4fb6b8e9a consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSchemeScheduleTest.java
|
||||||
44b0010772765eacb8fb720cc0570b0a8f681a3a329060271e67b7c518ddb25b consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java
|
e73a075b2acfce7a6d2de63130ed55a3d8f422bdc72c51325a010852064d406d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSealPersistenceTest.java
|
||||||
e0bc7a465dbb2dd7efa5f1830211a1a45c6554c300b2088dd70bb9677b0ac217 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java
|
e0bc7a465dbb2dd7efa5f1830211a1a45c6554c300b2088dd70bb9677b0ac217 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqSignedHeightTest.java
|
||||||
9a371ec367cda846640c6da18c8bf7d921a6882cfc024dc0c86d4f965cae0c63 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java
|
9a371ec367cda846640c6da18c8bf7d921a6882cfc024dc0c86d4f965cae0c63 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqStartupHistoryTest.java
|
||||||
e65ba92e288aebe768909b1fee5c0b8850be74b3658c6242e6abdb91409a9521 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java
|
e65ba92e288aebe768909b1fee5c0b8850be74b3658c6242e6abdb91409a9521 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/PqV2Fixture.java
|
||||||
@ -74,7 +99,9 @@ e65ba92e288aebe768909b1fee5c0b8850be74b3658c6242e6abdb91409a9521 consensus/comm
|
|||||||
e355b147fdda48c14cb6dc09b1bc1e7a7ccae3f9b709af8f8483f896086dbe4d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/ProposalPqAttachGateTest.java
|
e355b147fdda48c14cb6dc09b1bc1e7a7ccae3f9b709af8f8483f896086dbe4d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/ProposalPqAttachGateTest.java
|
||||||
45a881d122a7ffa5ff4d2db38a2750e3918055593c51e1b1241342905a30565d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/RoundChangePqAttachGateTest.java
|
45a881d122a7ffa5ff4d2db38a2750e3918055593c51e1b1241342905a30565d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/RoundChangePqAttachGateTest.java
|
||||||
d48d62c6ea0784864a5987a804a28855265b2180c409ba3c5b4434d21b805e1d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java
|
d48d62c6ea0784864a5987a804a28855265b2180c409ba3c5b4434d21b805e1d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java
|
||||||
22ade1aa2d8254d1f7ff02bbbcb4d02155546a52fca3421b61eea1129b73ee48 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java
|
184e3cc6f2f9beb1bd972b077ab2ced456264a55296f810ba5ed2dc976195392 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SlhDsaCrossVectorTest.java
|
||||||
|
ae37d3525697bf84defd796bb8813193c78f81e7fa24a72f8f048285d52399e2 consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SlhDsaFastEngineTest.java
|
||||||
|
6b6ecfcfb9b25bb009cb5d4df74648c729abf62626e5034af126771825b7418e consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java
|
||||||
597d96d64c9394c79d5084a1ab5df884b2266efe315c0f2d563bfc17b7421480 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java
|
597d96d64c9394c79d5084a1ab5df884b2266efe315c0f2d563bfc17b7421480 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java
|
||||||
03173f2029f767e10cc978b5576b9557efa783dab2ef105aa856f6d5e08b9352 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java
|
03173f2029f767e10cc978b5576b9557efa783dab2ef105aa856f6d5e08b9352 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java
|
||||||
d365bad33f3aee4597c630071ca1f727ddd8bb6ab6515fccbce0a07842037adf consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java
|
d365bad33f3aee4597c630071ca1f727ddd8bb6ab6515fccbce0a07842037adf consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java
|
||||||
@ -82,12 +109,13 @@ d365bad33f3aee4597c630071ca1f727ddd8bb6ab6515fccbce0a07842037adf consensus/qbft
|
|||||||
428e11639fb24ae4b04972457714445a5cd59d7946135a8b7362123461fa690f consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java
|
428e11639fb24ae4b04972457714445a5cd59d7946135a8b7362123461fa690f consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java
|
||||||
2e133e92dabb6b7add09820f9239050bd8d3b9ad63c16ed7f6e725a9694f2232 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java
|
2e133e92dabb6b7add09820f9239050bd8d3b9ad63c16ed7f6e725a9694f2232 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayload.java
|
||||||
51eddf3cb8e37489abd35e6f4c36294a13ec76337bbbbfd54c07584085dceaac consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java
|
51eddf3cb8e37489abd35e6f4c36294a13ec76337bbbbfd54c07584085dceaac consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java
|
||||||
252f9a86ca17cc6362780af264dbc4da4d842191467427a809ff1e4baa7640af consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java
|
7fef8f228638187615de0c7c92f1ae41a83a9c2997e64ef527f125a204e40204 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java
|
||||||
609890459ba6be78d0b91c04e80927231e8d61a0af822c4049ff0972530ab7ea consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
|
9c7e2698016f8cf19d3b6d78c6256ad5e8eabec06d9a877eee4e8c2dde423839 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java
|
||||||
a22d857867c5b7c9ccdf9185c661870eafe6b481573189202b2cc34d9fbf3e2d consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java
|
00c8204e4b44acd469973d6bbc07992382fdd2d692f079a55de76f944a7432f8 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java
|
||||||
601b0cccf32f8ebe327c7581e0e9b3f9f67de49d9fb646326d6b49ef2138c641 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java
|
601b0cccf32f8ebe327c7581e0e9b3f9f67de49d9fb646326d6b49ef2138c641 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java
|
||||||
7f9fb13a7bbe3015bbef6329f750f2b35fb3744895cd3e6599af90450fab6a73 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java
|
01ed4a44aa58da124efb6f4fcf479f9511c8f522f40ccf0be5fff3d0bcb38f34 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java
|
||||||
ec53f8e401a19345a067223f482f27a95cb9c3153d5bee0bfea9fd622bc0db01 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java
|
e533a19b256f7d9a64980af283bd8ce148bc8ae3c297a3cd92908c9d1a70981e consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/MessageValidator.java
|
||||||
|
bebf53cbc7f3a9ca2807bd980d313f8131e96f79259ba41e80a598d0327b1a0d consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcement.java
|
||||||
5ab52433f770537e838b15e23abfc7b24eaa733ea26577451d5e32226d617879 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcement.java
|
5ab52433f770537e838b15e23abfc7b24eaa733ea26577451d5e32226d617879 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcement.java
|
||||||
742611d52091a6c138cdff60c5f7f0bfb2fc33e567f520f3e86878fc086e77d9 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcement.java
|
742611d52091a6c138cdff60c5f7f0bfb2fc33e567f520f3e86878fc086e77d9 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcement.java
|
||||||
afb487f8c1dd87e323fdc1153ecfdf8ec6cb5a10e0726783d4214c07a3fa9a95 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqRoundChangeEnforcement.java
|
afb487f8c1dd87e323fdc1153ecfdf8ec6cb5a10e0726783d4214c07a3fa9a95 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqRoundChangeEnforcement.java
|
||||||
@ -100,11 +128,15 @@ e2107737579c16b73cbd2c9b50e90abcd6c910309286fb862cef780555ca43a5 consensus/qbft
|
|||||||
fe25255de4b5fa6d86412fa4c48218180f549f0a4ffea5b5cc0a7d19a5be4cd9 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayloadPqTest.java
|
fe25255de4b5fa6d86412fa4c48218180f549f0a4ffea5b5cc0a7d19a5be4cd9 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayloadPqTest.java
|
||||||
a994d5638940ea90f37ed6f3ed5eebc24781707f2f4555c7a079d1251ddbf893 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayloadPqTest.java
|
a994d5638940ea90f37ed6f3ed5eebc24781707f2f4555c7a079d1251ddbf893 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayloadPqTest.java
|
||||||
df5d7d2119cf0864a8c2dce50f4f69bd8b64804cfda7f7366c013b304af0a6e7 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayloadPqTest.java
|
df5d7d2119cf0864a8c2dce50f4f69bd8b64804cfda7f7366c013b304af0a6e7 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/RoundChangePayloadPqTest.java
|
||||||
|
97ac1e46ab37238a2b6232eccdd4636ac6fa928c8e3d4f8a473c083c5d5c3b39 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqHybridExtrasNeverThrowTest.java
|
||||||
0bb2c1e31a7f29e9b07fb7030567746fc6d79460ed4739d5ecafac44501f7bb5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqLateSealSalvageTest.java
|
0bb2c1e31a7f29e9b07fb7030567746fc6d79460ed4739d5ecafac44501f7bb5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqLateSealSalvageTest.java
|
||||||
|
127de5fbe8014768a657f31bc77d7e66557273febdf3412a5536e8628fd70c5c consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/statemachine/PqSealsWithoutProposalTest.java
|
||||||
|
3f945a0a73288e42c82026ead5d118ab6b7a8200aacc881855aff442d0954c1c consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidatorAnchorFormTest.java
|
||||||
8c40173c7b248edec7c29ee9e049f9e269f4725b637bceb8734735fc73eb9ca2 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidatorPqEnforcementTest.java
|
8c40173c7b248edec7c29ee9e049f9e269f4725b637bceb8734735fc73eb9ca2 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidatorPqEnforcementTest.java
|
||||||
|
3374a7ee317a807528c79b8218dbb34b308de8b7a908cb6680b83356641fcade consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/MessageValidatorAnchorFormPlumbingTest.java
|
||||||
401c25f63abb248dfe60b69676a34ea255bb669b4bc58e45916f6eecea3cb870 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcementTest.java
|
401c25f63abb248dfe60b69676a34ea255bb669b4bc58e45916f6eecea3cb870 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitEnforcementTest.java
|
||||||
c0853ed97c53d54951e25ad6d0b70c0299dedd859cc7c44da64e3751d0e0de33 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitPlumbingTest.java
|
c0853ed97c53d54951e25ad6d0b70c0299dedd859cc7c44da64e3751d0e0de33 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqCommitPlumbingTest.java
|
||||||
5e0bb0ecc77ffb06f232e1aa81cca6870e5455af7c2846ae54d5f477f1bbb88b consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridEnforcementTest.java
|
61f6f0ca66cabbad7e57d7eccf86a6d4f5434f0769726db24efec38c9503583d consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqHybridEnforcementTest.java
|
||||||
bca46ca91803c89eb18d670c740edb4e47ef0bca53a9b4b82ce0c8446e0d6c0e consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java
|
bca46ca91803c89eb18d670c740edb4e47ef0bca53a9b4b82ce0c8446e0d6c0e consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java
|
||||||
5a47d247d7bcda77b57f5c906c3cee1af826785416012fafe9cfa49d63671388 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareAgilityTest.java
|
5a47d247d7bcda77b57f5c906c3cee1af826785416012fafe9cfa49d63671388 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareAgilityTest.java
|
||||||
1939e33cc8ea81782e5e17d68e2b59c001e4207ed8ef07acd1d2a166a047dcb3 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcementTest.java
|
1939e33cc8ea81782e5e17d68e2b59c001e4207ed8ef07acd1d2a166a047dcb3 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqPrepareEnforcementTest.java
|
||||||
@ -116,14 +148,14 @@ dc9f9e862a11f0135d26976176a1d1adac3e84fd5f1e06572d8727262cb3374f consensus/qbft
|
|||||||
83dd971f66ed63103d09db5283240556bf1db1e1925c2a07808c7f59d47433ad consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangeJustificationPqTest.java
|
83dd971f66ed63103d09db5283240556bf1db1e1925c2a07808c7f59d47433ad consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangeJustificationPqTest.java
|
||||||
8f6abb4297df964e934cfeabe51f365478e917b73cd6909f621c9e201e280aa5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidatorPqWiringTest.java
|
8f6abb4297df964e934cfeabe51f365478e917b73cd6909f621c9e201e280aa5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangePayloadValidatorPqWiringTest.java
|
||||||
79dea5e85b968af696bf57d51d0de175a4c025537200402e3594a4a8290d1a29 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java
|
79dea5e85b968af696bf57d51d0de175a4c025537200402e3594a4a8290d1a29 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java
|
||||||
7d0a75818ac64b601b79c32b8289eedda8e2300dbb0f5e9186084f4e01b04d26 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java
|
42b86cd888efe1aa00f9c9477566e353649c609c13a042e514185b57a6674a33 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java
|
||||||
47fbdd8639c464bf19bac3a0ba540af4d99f79e9df02c2ad586a19a2aebd037d consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java
|
103d46eecfc11c97fb59a1e0237a6b5b6d384a43ec45f5028bd225b8bf8e3f6c consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/adaptor/QbftBlockCreatorAdaptor.java
|
||||||
cfb0aad408ab620f28b09914ece7d8d3d09f9e6bbb503070e28d9ef39c023dee consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/blockcreation/QbftBlockCreatorFactory.java
|
cfb0aad408ab620f28b09914ece7d8d3d09f9e6bbb503070e28d9ef39c023dee consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/blockcreation/QbftBlockCreatorFactory.java
|
||||||
cbec6f4e280250ddefe5c79c453b59a07be6435e6f88d5614e92168c855e4693 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRule.java
|
cbec6f4e280250ddefe5c79c453b59a07be6435e6f88d5614e92168c855e4693 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/AereBaseFeeImportRule.java
|
||||||
e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java
|
e4e75c28ff15d058176145b1ae32606bf91b22112d1668a718f1a6ae4add0e71 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/FalconSealValidationRule.java
|
||||||
ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestAttachedRule.java
|
ab0fcd8722dcb76560f0ef8fda8af9c2b8b6ec9b468326aaf616c853526f7f81 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestAttachedRule.java
|
||||||
81a46071e77eb72aac107e6afca9e50612a81b66c5f17999815652eeda005248 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java
|
63313f944fcda0db45c40d4df840a465e6147cc85997b8ca832f0cfefea97987 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDigestRule.java
|
||||||
05ee2e97644c9a79ee082a5030b6e2a6745933d26cb8238a6339e6006c47d7e3 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
|
bfea357d0f539ae7d2d6fabf2ea343e0bce3d3faf2cea57c6d5e97c75552371b consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRule.java
|
||||||
2add2a7733668e11b50ddd64b05bb37db26a18284a636bbe720c2cb1f6aa28ed consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.java
|
2add2a7733668e11b50ddd64b05bb37db26a18284a636bbe720c2cb1f6aa28ed consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRule.java
|
||||||
8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java
|
8ac99e94c89f61f0281199cf369e282fc4f9fb1a3414a1650766e1d7548e76f5 consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqRegistryBindingRule.java
|
||||||
52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java
|
52b56f157500ae3527b7e55c51786915bb5d6980065668f1e7b8e297bf7125ef consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftAnchorRuleWiringTest.java
|
||||||
@ -134,6 +166,7 @@ e2df575ee4d6ab5bd961b0886ece3d3c392a50193f1d3256a438a72da9e20d10 consensus/qbft
|
|||||||
77d16fd35ca5607e9702f39cfd9a24a7e8523f631a73e82138763e6d5b718e7b consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
|
77d16fd35ca5607e9702f39cfd9a24a7e8523f631a73e82138763e6d5b718e7b consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorDisarmedWindowTest.java
|
||||||
a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java
|
a81da71dd34dd111a0da43c22d22345dcc595c084a2c2d36aec1f45d8b762f80 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorSealsRuleTest.java
|
||||||
535cca4f1083c8a25bd29877e988b99704666853c1fe8b0ba32d71efe4e15f37 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java
|
535cca4f1083c8a25bd29877e988b99704666853c1fe8b0ba32d71efe4e15f37 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorTestSupport.java
|
||||||
|
612d5c7578a918441265a6ad99bf05e9c5e94eec743c375ffab03d1b19e2f797 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqAnchorV2RulesTest.java
|
||||||
321cb7e6923f77e078b523316650ac49136cf8d4df506b9bb235a32795c69611 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java
|
321cb7e6923f77e078b523316650ac49136cf8d4df506b9bb235a32795c69611 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqArmedWithoutRegistryTest.java
|
||||||
484bc74ff3d1e52d25631264d83af70c2b695e973c7360b0f3db471ab5c6613a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.java
|
484bc74ff3d1e52d25631264d83af70c2b695e973c7360b0f3db471ab5c6613a consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqEmergencyShoutRuleTest.java
|
||||||
49c3fd97e7985966531928af173b6f38ac48ff9dc8b5be1106e5b7bfb9c7b2b6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java
|
49c3fd97e7985966531928af173b6f38ac48ff9dc8b5be1106e5b7bfb9c7b2b6 consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/PqForkGateFeedTest.java
|
||||||
|
|||||||
@ -26,6 +26,22 @@ should not be confused:
|
|||||||
startup guard refuses any threshold above N - f = 7, the highest count reachable with the fault
|
startup guard refuses any threshold above N - f = 7, the highest count reachable with the fault
|
||||||
budget down. Blocks between anchors carry no Falcon seals and are finalized by ECDSA alone, so
|
budget down. Blocks between anchors carry no Falcon seals and are finalized by ECDSA alone, so
|
||||||
the post-quantum guarantee is a checkpoint about every 16 seconds, not a per-block quorum.
|
the post-quantum guarantee is a checkpoint about every 16 seconds, not a per-block quorum.
|
||||||
|
3. **The hybrid certificate.** Since block 17,047,600 (2026-09-04) the certificate is the v2, scheme-tagged
|
||||||
|
form `RLP[2, [[scheme, index, signature], ...]]` and carries Falcon-512 (lattice) AND SLH-DSA-SHA2-128s
|
||||||
|
(hash-based) seals; the enforced minimum applies to EACH scheme on its own, at least six of nine valid
|
||||||
|
seals of each, and every SLH-DSA seal must sit at an index that also carries a valid Falcon seal. Properties:
|
||||||
|
`aere.pq.anchorV2Block=17047600`, `aere.pq.hybrid.attachBlock=17047568`,
|
||||||
|
`aere.pq.schemeSchedule=17047568:falcon-512+slh-dsa-sha2-128s`, `aere.pq.hybridRegistry` (the epoch-2 key
|
||||||
|
manifest, also published under `/pq/manifests/`). The fleet schedule carries one dated step,
|
||||||
|
`17102384:0,17102416:6`: on 2026-09-04 a run of validator restarts emptied every node's seal store, the
|
||||||
|
chain stalled at anchor 17,102,384 for eleven minutes, and it was released by lowering the minimum to
|
||||||
|
zero at exactly that height and restoring six at the next anchor. The step stays in the schedule because
|
||||||
|
every node must agree on that history.
|
||||||
|
4. **The anchor interval.** `aere.pq.anchorIntervalSchedule=17225968:128`, set on all nine validators and
|
||||||
|
on every reading node on 2026-09-05: from block 17,225,968 an anchor falls on every 128th block instead
|
||||||
|
of every 32nd. Each hybrid certificate adds about 53 KB to its header; quartering the frequency divides
|
||||||
|
that cost by four without touching the rule, the thresholds or the schemes. Until that height the
|
||||||
|
interval is 32, and the checkpoint above becomes one about every 64 seconds after it.
|
||||||
|
|
||||||
Correction 2026-08-19: the fleet was also configured on 2026-08-15 with `aere.falcon.forkBlock`
|
Correction 2026-08-19: the fleet was also configured on 2026-08-15 with `aere.falcon.forkBlock`
|
||||||
at 14,050,000, intended as a per-block blocking Falcon quorum. In the shipped code that rule
|
at 14,050,000, intended as a per-block blocking Falcon quorum. In the shipped code that rule
|
||||||
@ -35,12 +51,17 @@ inert, and the chain itself confirms it: blocks between anchors carry no Falcon
|
|||||||
per-block quorum rule would have rejected. The earlier statement in this repository that from
|
per-block quorum rule would have rejected. The earlier statement in this repository that from
|
||||||
14,050,000 every block required a 2f+1 Falcon quorum to finalize is withdrawn.
|
14,050,000 every block required a 2f+1 Falcon quorum to finalize is withdrawn.
|
||||||
|
|
||||||
"Hybrid" is the honest word only in the checkpoint sense above, and "post-quantum consensus"
|
"Hybrid" is the honest word in two senses and no more: the checkpoint sense above and, since
|
||||||
without that qualification overstates: block proposal, validator identity and message transport
|
2026-09-04, the two-scheme certificate. "Post-quantum consensus" without qualification still
|
||||||
are classical elliptic-curve cryptography, and a cryptographically relevant quantum computer would
|
overstates: per-block finality and validator identity are classical elliptic-curve cryptography. The
|
||||||
break those exactly as on any other ECDSA chain. What it cannot do is rewrite history past the
|
four QBFT message types carry a post-quantum seal of their author on chain 2800 since blocks
|
||||||
last anchor without also forging Falcon-512 signatures. None of this has been audited by a third
|
16,620,000 (PROPOSAL), 16,660,000 (ROUND-CHANGE) and 16,690,000 (PREPARE), COMMIT seals since the
|
||||||
party.
|
anchor itself; and a message without a valid seal is REFUSED from blocks 17,250,000 (COMMIT),
|
||||||
|
17,400,000 (PROPOSAL), 17,550,000 (ROUND-CHANGE) and 17,700,000 (PREPARE). Those four enforcement
|
||||||
|
heights were set on all nine validators on 2026-09-05 and take effect as the chain reaches them;
|
||||||
|
until each height passes, that message type is still accepted without a seal. What a quantum
|
||||||
|
adversary cannot do at any height is rewrite history past the last anchor without also forging
|
||||||
|
Falcon-512 and SLH-DSA-SHA2-128s signatures. None of this has been audited by a third party.
|
||||||
|
|
||||||
The threat it addresses is not "harvest now, decrypt later". A signature is public; there is nothing
|
The threat it addresses is not "harvest now, decrypt later". A signature is public; there is nothing
|
||||||
to harvest. The threat is **retroactive rewriting**: validator keys recovered later can be used to
|
to harvest. The threat is **retroactive rewriting**: validator keys recovered later can be used to
|
||||||
@ -145,13 +166,16 @@ This matters more than the file list, so it is stated before it.
|
|||||||
| Anchor certificate under the block hash | yes | **yes**, since block 13,014,000 |
|
| Anchor certificate under the block hash | yes | **yes**, since block 13,014,000 |
|
||||||
| Enforced minimum seals at an anchor height | yes | **yes**, 6 of 9 since 14,961,456 |
|
| Enforced minimum seals at an anchor height | yes | **yes**, 6 of 9 since 14,961,456 |
|
||||||
| Legacy per-block Falcon rule (`aere.falcon.forkBlock`) | yes | **no** — retired at the anchor block |
|
| Legacy per-block Falcon rule (`aere.falcon.forkBlock`) | yes | **no** — retired at the anchor block |
|
||||||
| Post-quantum seal on PREPARE, emission | yes | **no** — no node sets the property |
|
| Post-quantum seal on PREPARE, emission | yes | **yes**, since block 16,690,000 |
|
||||||
| Post-quantum seal on PREPARE, enforcement | yes | **no** — no node sets the property |
|
| Post-quantum seal on PREPARE, enforcement | yes | **armed for block 17,700,000** (set 2026-09-05) |
|
||||||
| Post-quantum seal on the PROPOSAL, emission | yes | **no** — no node sets the property |
|
| Post-quantum seal on the PROPOSAL, emission | yes | **yes**, since block 16,620,000 |
|
||||||
| Post-quantum seal on the PROPOSAL, enforcement | yes | **no** — no node sets the property |
|
| Post-quantum seal on the PROPOSAL, enforcement | yes | **armed for block 17,400,000** (set 2026-09-05) |
|
||||||
| Hybrid Falcon + SLH-DSA certificate | yes | **no** — needs new keys, not generated |
|
| Post-quantum seal on ROUND-CHANGE, emission / enforcement | yes | **yes**, since block 16,660,000 / **armed for block 17,550,000** |
|
||||||
|
| Post-quantum seal on COMMIT, emission / enforcement | yes | **yes**, since the anchor / **armed for block 17,250,000** |
|
||||||
|
| Hybrid Falcon + SLH-DSA certificate (v2) | yes | **yes**, since block 17,047,600 (2026-09-04) |
|
||||||
|
| Anchor interval 128 (`aere.pq.anchorIntervalSchedule`) | yes | **armed for block 17,225,968** (set 2026-09-05) |
|
||||||
|
|
||||||
Everything in the "no" rows is **disarmed by absence, not by a flag**: the properties that arm them
|
Everything not yet in force is **disarmed by absence or by height, never by a flag**: the properties that arm them
|
||||||
are unset, and unset means never. Each refuses loudly on a value it cannot parse rather than booting
|
are unset, and unset means never. Each refuses loudly on a value it cannot parse rather than booting
|
||||||
a node that believes itself armed — a node that disarms itself because of a mistyped character looks
|
a node that believes itself armed — a node that disarms itself because of a mistyped character looks
|
||||||
exactly like a correctly configured one, right up to the day it matters. The tests for that
|
exactly like a correctly configured one, right up to the day it matters. The tests for that
|
||||||
@ -159,8 +183,9 @@ behaviour are in this directory and they are the ones to read if you doubt the c
|
|||||||
|
|
||||||
The PREPARE layer is newer than the anchor and stronger where it applies: an armed node that refuses
|
The PREPARE layer is newer than the anchor and stronger where it applies: an armed node that refuses
|
||||||
unsealed PREPAREs never reaches the prepared state, so it never sends COMMIT at all. That also means
|
unsealed PREPAREs never reaches the prepared state, so it never sends COMMIT at all. That also means
|
||||||
it has no safety net during an activation, which is why it is not armed anywhere and why its
|
it has no safety net during an activation, which is why it was armed LAST and highest: its
|
||||||
activation height is a decision that has not been taken.
|
enforcement height on chain 2800 is 17,700,000, set on 2026-09-05 after the COMMIT, PROPOSAL and
|
||||||
|
ROUND-CHANGE heights, so that the three other layers are in force before it.
|
||||||
|
|
||||||
The PROPOSAL layer (added 2026-08-30) closes the remaining hot-path gap, and its claim is stated
|
The PROPOSAL layer (added 2026-08-30) closes the remaining hot-path gap, and its claim is stated
|
||||||
precisely because the imprecise version oversells: an adversary who breaks ECDSA cannot finalize
|
precisely because the imprecise version oversells: an adversary who breaks ECDSA cannot finalize
|
||||||
@ -168,9 +193,9 @@ anything while the PREPARE layer is armed, since "prepared" needs a full quorum
|
|||||||
forged proposals could still do is OPEN rounds and waste them. With proposal enforcement armed, a
|
forged proposals could still do is OPEN rounds and waste them. With proposal enforcement armed, a
|
||||||
proposal without a valid Falcon seal from its own proposer does not open a round. The seal signs
|
proposal without a valid Falcon seal from its own proposer does not open a round. The seal signs
|
||||||
its own domain over (chainId, height, round, digest), so an honest proposal seal cannot be replayed
|
its own domain over (chainId, height, round, digest), so an honest proposal seal cannot be replayed
|
||||||
as a vote nor a vote seal as a proposal - both directions are tested. What remains classical: the
|
as a vote nor a vote seal as a proposal - both directions are tested. The ROUND-CHANGE message
|
||||||
ROUND-CHANGE message itself (its embedded justifications are already coupled to PREPARE
|
carries its own seal since 2026-08-31 (its embedded justifications are coupled to PREPARE
|
||||||
enforcement) and node-level devp2p authentication.
|
enforcement). What remains classical: node-level devp2p authentication and validator identity.
|
||||||
|
|
||||||
### The files
|
### The files
|
||||||
|
|
||||||
@ -194,9 +219,16 @@ already take the dependency in exactly this versionless form. So the line adds a
|
|||||||
and no new artifact, and it does not move any version. That is the entire build change, and it is
|
and no new artifact, and it does not move any version. That is the entire build change, and it is
|
||||||
called out here rather than left to be found in the diff.
|
called out here rather than left to be found in the diff.
|
||||||
|
|
||||||
No cryptography is implemented in this overlay. Falcon signature verification calls Bouncy Castle's
|
Falcon signature verification calls Bouncy Castle's implementation. SLH-DSA-SHA2-128s signing and
|
||||||
implementation; what is ours is the framing, the registry that maps a validator to a key, the digest,
|
verification use the `slhdsa/` package in `consensus/common`: Bouncy Castle 1.80's own SLH-DSA
|
||||||
and the validation rules.
|
implementation, copied into the tree with one change, the SHA-256 and SHA-512 digests come from the
|
||||||
|
JDK (`MessageDigest`, hardware SHA extensions) instead of Bouncy Castle's pure-Java digests, because
|
||||||
|
the pure-Java path made one anchor signature cost 1.4 to 2.7 seconds on the consensus thread
|
||||||
|
(measured 2026-09-04). Signatures are byte-identical to the original in deterministic mode, which
|
||||||
|
`SlhDsaFastEngineTest` pins with cross-verification in both directions. The package is regenerated
|
||||||
|
from the published Bouncy Castle sources by a script, not edited by hand, and carries the Bouncy
|
||||||
|
Castle licence next to it (`slhdsa/LICENSE-BouncyCastle.txt`). What is ours is the framing, the
|
||||||
|
registry that maps a validator to its keys, the digest, the scheme schedule and the validation rules.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -225,14 +257,15 @@ and the validation rules.
|
|||||||
upgraded every node.
|
upgraded every node.
|
||||||
- **Nothing here demonstrates what is configured on any live network.** These files show what the
|
- **Nothing here demonstrates what is configured on any live network.** These files show what the
|
||||||
code does when armed. They are not evidence about any running fleet, and should not be read as any.
|
code does when armed. They are not evidence about any running fleet, and should not be read as any.
|
||||||
- **The PREPARE layer has not run on a live network.** It has been exercised on a test network,
|
- **The message-layer ENFORCEMENT has not yet refused anything on chain 2800.** Emission runs on the
|
||||||
including a mixed run against a second, independent client implementation, and it has not been
|
live fleet; enforcement is armed by height (see the table) and, at the time of writing, none of the
|
||||||
armed on chain 2800 or anywhere else that carries value. Test-network evidence is evidence about a
|
four heights has passed. The refusal path has been exercised on the public testnet 28001, including
|
||||||
test network.
|
against a second, independent client implementation; test-network evidence is evidence about a test
|
||||||
- **The hybrid Falcon + SLH-DSA certificate has never been signed with a real key.** The scheme
|
network.
|
||||||
layer is here and a second algorithm passes through the same consensus code untouched, which is
|
- **The hybrid certificate is live but young.** Hybrid certificates have been produced on chain 2800
|
||||||
what the tests measure. Generating hybrid validator keys is a separate decision that has not been
|
since 2026-09-04 with real validator keys, and the public verifier reproduces them from the
|
||||||
taken, so no hybrid certificate exists on any chain.
|
internet. What is not shown here is any third-party review of the SLH-DSA engine copy or of the
|
||||||
|
scheme layer.
|
||||||
|
|
||||||
## One claim we retracted, on purpose
|
## One claim we retracted, on purpose
|
||||||
|
|
||||||
@ -275,4 +308,6 @@ each seal costs bytes in every header forever.
|
|||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Apache 2.0, matching upstream Hyperledger Besu. See `../LICENSE` and `../NOTICE`. Files that modify
|
Apache 2.0, matching upstream Hyperledger Besu. See `../LICENSE` and `../NOTICE`. Files that modify
|
||||||
upstream carry the change notice required by section 4(b); files that are new are ours.
|
upstream carry the change notice required by section 4(b); files that are new are ours. The `slhdsa/`
|
||||||
|
package in `consensus/common` is derived from Bouncy Castle and stays under the Bouncy Castle licence
|
||||||
|
(MIT-style); its notice is kept next to the code in `LICENSE-BouncyCastle.txt`.
|
||||||
|
|||||||
@ -41,6 +41,7 @@ import org.hyperledger.besu.consensus.common.bft.BlockTimer;
|
|||||||
import org.hyperledger.besu.consensus.common.bft.EthSynchronizerUpdater;
|
import org.hyperledger.besu.consensus.common.bft.EthSynchronizerUpdater;
|
||||||
import org.hyperledger.besu.consensus.common.bft.EventMultiplexer;
|
import org.hyperledger.besu.consensus.common.bft.EventMultiplexer;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
|
import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.HybridSealSupport;
|
||||||
import org.hyperledger.besu.consensus.common.bft.MessageTracker;
|
import org.hyperledger.besu.consensus.common.bft.MessageTracker;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchorSyncModeGuard;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorSyncModeGuard;
|
||||||
@ -235,6 +236,14 @@ public class QbftBesuControllerBuilder extends BesuControllerBuilder {
|
|||||||
.orElse(false),
|
.orElse(false),
|
||||||
dataDirectory);
|
dataDirectory);
|
||||||
|
|
||||||
|
// D-334 (2026-09-03): the HYBRID configuration (scheme schedule, registry, SLH-DSA key) is
|
||||||
|
// loaded HERE, at startup, and not on first use. Measured on mainnet 2800: a registry the
|
||||||
|
// container user could not read passed every configuration gate (the file existed) and blew up
|
||||||
|
// inside the QBFT state machine at every anchor parent on three armed validators; with a fourth
|
||||||
|
// node restarting the chain stood 125 s. A defect in this configuration must refuse to START,
|
||||||
|
// exactly like the Falcon key above; QbftRound.hybridExtrasOrEmpty is the second net.
|
||||||
|
HybridSealSupport.instance();
|
||||||
|
|
||||||
// AERE GARDA-CHEIE-MASINA (2026-08-07): the Falcon key loaded must be THIS machine's own.
|
// AERE GARDA-CHEIE-MASINA (2026-08-07): the Falcon key loaded must be THIS machine's own.
|
||||||
//
|
//
|
||||||
// Measured on seven test nodes with a single variable changed: node 0 was handed index 1's key.
|
// Measured on seven test nodes with a single variable changed: node 0 was handed index 1's key.
|
||||||
|
|||||||
@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to 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 org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-337 (2026-09-04): how long the proposer waits, ONLY at an anchor height, before its proposal
|
||||||
|
* timer fires.
|
||||||
|
*
|
||||||
|
* <p>WHY THIS EXISTS, measured on chain 2800. At an anchor height the proposer must carry K valid
|
||||||
|
* seals of EVERY scheme in the schedule, and the SLH-DSA seals arrive with the commits of the
|
||||||
|
* parent. With the hybrid certificate live, the proposal timer fires about half a second after the
|
||||||
|
* parent and the sixth SLH-DSA seal is not there yet, so the producer refuses (it will not write a
|
||||||
|
* certificate it cannot fill), round 0 expires, and the anchor costs the whole four-second
|
||||||
|
* round-change timeout: eleven of twelve anchors measured on 2026-09-04, parent-to-anchor 5.6 s
|
||||||
|
* instead of 0.8 s. Making the signature four times faster fixed the PARENT (3.3 s to 1 s) and did
|
||||||
|
* not fix this, because the race is against the proposal timer, not against the CPU.
|
||||||
|
*
|
||||||
|
* <p>WHAT IT IS NOT. This is local timing, not consensus. A node that does not set it proposes
|
||||||
|
* exactly as before; a proposal that arrives later is valid under the same rules (QBFT only
|
||||||
|
* requires the timestamp to be at least a block period after the parent). So it can be set node by
|
||||||
|
* node, with no activation height and no fleet agreement, and a wrong value costs liveness at
|
||||||
|
* anchors, never a fork.
|
||||||
|
*
|
||||||
|
* <p>Unset or zero means today's behaviour, exactly. The value is read once, validated at node
|
||||||
|
* start (from {@link BlockTimer}'s constructor), and refused if it is negative or above five
|
||||||
|
* seconds: a delay longer than the round-change timeout would trade one stall for another.
|
||||||
|
*/
|
||||||
|
public final class AereAnchorProposalDelay {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AereAnchorProposalDelay.class);
|
||||||
|
|
||||||
|
/** System property carrying the delay in milliseconds. */
|
||||||
|
public static final String PROPERTY = "aere.pq.anchorProposalDelayMs";
|
||||||
|
|
||||||
|
/** Environment variable carrying the delay in milliseconds. */
|
||||||
|
public static final String ENV = "AERE_PQ_ANCHOR_PROPOSAL_DELAY_MS";
|
||||||
|
|
||||||
|
/** The largest delay that can be configured; above this a stall is traded for a stall. */
|
||||||
|
public static final long MAX_MILLIS = 5000L;
|
||||||
|
|
||||||
|
private static volatile Long cached;
|
||||||
|
|
||||||
|
private AereAnchorProposalDelay() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The configured delay in milliseconds, read once and memoised.
|
||||||
|
*
|
||||||
|
* @return the delay, zero when unset
|
||||||
|
* @throws IllegalArgumentException if the value is not a number in [0, {@link #MAX_MILLIS}]
|
||||||
|
*/
|
||||||
|
public static long configuredMillis() {
|
||||||
|
Long local = cached;
|
||||||
|
if (local == null) {
|
||||||
|
synchronized (AereAnchorProposalDelay.class) {
|
||||||
|
local = cached;
|
||||||
|
if (local == null) {
|
||||||
|
local = parse(readRaw());
|
||||||
|
if (local > 0) {
|
||||||
|
LOG.info(
|
||||||
|
"AERE PQ-ANCHOR: proposals at anchor heights wait an extra {} ms for the seals of"
|
||||||
|
+ " the parent (D-337). This is local timing, not consensus.",
|
||||||
|
local);
|
||||||
|
}
|
||||||
|
cached = local;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return local;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The delay to add to the proposal timer for a given height: the configured value at an anchor
|
||||||
|
* height, zero everywhere else.
|
||||||
|
*
|
||||||
|
* @param blockNumber the height whose proposal timer is being started
|
||||||
|
* @return milliseconds to add
|
||||||
|
*/
|
||||||
|
public static long millisFor(final long blockNumber) {
|
||||||
|
final long delay = configuredMillis();
|
||||||
|
if (delay <= 0) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return PqAnchorProducer.config().anchorAppliesAt(blockNumber) ? delay : 0L;
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
// The anchor configuration is read at start and refuses there; if it somehow cannot be read
|
||||||
|
// here, the honest answer is "no extra delay", never an exception into the block timer.
|
||||||
|
LOG.warn("AERE PQ-ANCHOR: cannot tell whether {} is an anchor height: {}", blockNumber, e.getMessage());
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and validate a raw value.
|
||||||
|
*
|
||||||
|
* @param raw the text, possibly null or blank
|
||||||
|
* @return the delay in milliseconds
|
||||||
|
*/
|
||||||
|
static long parse(final String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
final long value;
|
||||||
|
try {
|
||||||
|
value = Long.parseLong(raw.trim());
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE-PQC-ANCHOR-CONF-05: " + PROPERTY + "='" + raw + "' is not a number of milliseconds");
|
||||||
|
}
|
||||||
|
if (value < 0 || value > MAX_MILLIS) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE-PQC-ANCHOR-CONF-05: "
|
||||||
|
+ PROPERTY
|
||||||
|
+ "="
|
||||||
|
+ value
|
||||||
|
+ " is outside [0, "
|
||||||
|
+ MAX_MILLIS
|
||||||
|
+ "] ms. A delay longer than the round-change timeout trades one stall for another.");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readRaw() {
|
||||||
|
final String property = System.getProperty(PROPERTY);
|
||||||
|
return property != null ? property : System.getenv(ENV);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop the memoised value. For tests only.
|
||||||
|
*/
|
||||||
|
static void forgetForTesting() {
|
||||||
|
synchronized (AereAnchorProposalDelay.class) {
|
||||||
|
cached = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -113,7 +113,11 @@ public class BftBlockInterface implements BlockInterface {
|
|||||||
prevExtraData.getVote(),
|
prevExtraData.getVote(),
|
||||||
round,
|
round,
|
||||||
prevExtraData.getValidators(),
|
prevExtraData.getValidators(),
|
||||||
prevExtraData.getFalconSeals());
|
prevExtraData.getFalconSeals(),
|
||||||
|
// AERE ANCHOR V2 (2026-09-03, D-328): the scheme-tagged certificate rides along too. Without
|
||||||
|
// this line every v2 header lost its certificate at the round substitution and the fleet
|
||||||
|
// refused its own first v2 anchor (testnet 28001, block 168032, 3 hours stalled).
|
||||||
|
prevExtraData.getHybridSeals());
|
||||||
|
|
||||||
final BlockHeaderBuilder headerBuilder = BlockHeaderBuilder.fromHeader(block.getHeader());
|
final BlockHeaderBuilder headerBuilder = BlockHeaderBuilder.fromHeader(block.getHeader());
|
||||||
headerBuilder
|
headerBuilder
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import org.hyperledger.besu.ethereum.core.ParsedExtraData;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.apache.tuweni.bytes.Bytes;
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
@ -46,6 +47,8 @@ public class BftExtraData implements ParsedExtraData {
|
|||||||
* committed seals or the block/header hash.
|
* committed seals or the block/header hash.
|
||||||
*/
|
*/
|
||||||
private final Collection<FalconSeal> falconSeals;
|
private final Collection<FalconSeal> falconSeals;
|
||||||
|
/** AERE ANCHOR V2: the scheme-tagged certificate; empty on every header below the v2 height. */
|
||||||
|
private final List<SchemeSeal> hybridSeals;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new Bft extra data (no Falcon seals). Retained for callers, notably the IBFT
|
* Instantiates a new Bft extra data (no Falcon seals). Retained for callers, notably the IBFT
|
||||||
@ -83,7 +86,30 @@ public class BftExtraData implements ParsedExtraData {
|
|||||||
final int round,
|
final int round,
|
||||||
final Collection<Address> validators,
|
final Collection<Address> validators,
|
||||||
final Collection<FalconSeal> falconSeals) {
|
final Collection<FalconSeal> falconSeals) {
|
||||||
|
this(vanityData, seals, vote, round, validators, falconSeals, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full constructor.
|
||||||
|
*
|
||||||
|
* @param hybridSeals the scheme-tagged (v2) certificate, or empty; a header carries the v1 Falcon
|
||||||
|
* list OR the v2 certificate, never both
|
||||||
|
*/
|
||||||
|
public BftExtraData(
|
||||||
|
final Bytes vanityData,
|
||||||
|
final Collection<SECPSignature> seals,
|
||||||
|
final Optional<Vote> vote,
|
||||||
|
final int round,
|
||||||
|
final Collection<Address> validators,
|
||||||
|
final Collection<FalconSeal> falconSeals,
|
||||||
|
final List<SchemeSeal> hybridSeals) {
|
||||||
checkNotNull(vanityData);
|
checkNotNull(vanityData);
|
||||||
|
checkNotNull(hybridSeals);
|
||||||
|
if (!hybridSeals.isEmpty() && falconSeals != null && !falconSeals.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE ANCHOR V2: a header carries the v1 Falcon list OR the v2 certificate, not both");
|
||||||
|
}
|
||||||
|
this.hybridSeals = List.copyOf(hybridSeals);
|
||||||
checkNotNull(seals);
|
checkNotNull(seals);
|
||||||
checkNotNull(validators);
|
checkNotNull(validators);
|
||||||
checkNotNull(falconSeals);
|
checkNotNull(falconSeals);
|
||||||
@ -149,6 +175,15 @@ public class BftExtraData implements ParsedExtraData {
|
|||||||
return falconSeals;
|
return falconSeals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scheme-tagged (v2) certificate.
|
||||||
|
*
|
||||||
|
* @return the v2 seals, canonical order; empty below the v2 height
|
||||||
|
*/
|
||||||
|
public List<SchemeSeal> getHybridSeals() {
|
||||||
|
return hybridSeals;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "BftExtraData{"
|
return "BftExtraData{"
|
||||||
@ -164,6 +199,8 @@ public class BftExtraData implements ParsedExtraData {
|
|||||||
+ round
|
+ round
|
||||||
+ ", falconSeals="
|
+ ", falconSeals="
|
||||||
+ falconSeals
|
+ falconSeals
|
||||||
|
+ ", hybridSeals="
|
||||||
|
+ hybridSeals
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* Copyright ConsenSys AG.
|
||||||
|
*
|
||||||
|
* 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 org.hyperledger.besu.config.BftConfigOptions;
|
||||||
|
import org.hyperledger.besu.consensus.common.ForksSchedule;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.events.BlockTimerExpiry;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/** Class for starting and keeping organised block timers */
|
||||||
|
public class BlockTimer {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(BlockTimer.class);
|
||||||
|
|
||||||
|
private final ForksSchedule<? extends BftConfigOptions> forksSchedule;
|
||||||
|
private final BftExecutors bftExecutors;
|
||||||
|
private Optional<ScheduledFuture<?>> currentTimerTask;
|
||||||
|
private final BftEventQueue queue;
|
||||||
|
private final Clock clock;
|
||||||
|
private long blockPeriodSeconds;
|
||||||
|
private long emptyBlockPeriodSeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a BlockTimer with primed executor service ready to start timers
|
||||||
|
*
|
||||||
|
* @param queue The queue in which to put block expiry events
|
||||||
|
* @param forksSchedule Bft fork schedule that contains block period seconds
|
||||||
|
* @param bftExecutors Executor services that timers can be scheduled with
|
||||||
|
* @param clock System clock
|
||||||
|
*/
|
||||||
|
public BlockTimer(
|
||||||
|
final BftEventQueue queue,
|
||||||
|
final ForksSchedule<? extends BftConfigOptions> forksSchedule,
|
||||||
|
final BftExecutors bftExecutors,
|
||||||
|
final Clock clock) {
|
||||||
|
// D-334: o configuratie gresita se refuza la PORNIRE, nu la prima intrebuintare in masina de stari
|
||||||
|
AereAnchorProposalDelay.configuredMillis();
|
||||||
|
this.queue = queue;
|
||||||
|
this.forksSchedule = forksSchedule;
|
||||||
|
this.bftExecutors = bftExecutors;
|
||||||
|
this.currentTimerTask = Optional.empty();
|
||||||
|
this.clock = clock;
|
||||||
|
this.blockPeriodSeconds = 0;
|
||||||
|
this.emptyBlockPeriodSeconds = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancels the current running round timer if there is one */
|
||||||
|
public synchronized void cancelTimer() {
|
||||||
|
currentTimerTask.ifPresent(t -> t.cancel(false));
|
||||||
|
currentTimerTask = Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether there is a timer currently running or not
|
||||||
|
*
|
||||||
|
* @return boolean of whether a timer is ticking or not
|
||||||
|
*/
|
||||||
|
public synchronized boolean isRunning() {
|
||||||
|
return currentTimerTask.map(t -> !t.isDone()).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a timer for the supplied round cancelling any previously active block timer
|
||||||
|
*
|
||||||
|
* @param round The round identifier which this timer is tracking
|
||||||
|
* @param headerTimestamp The timestamp from the of the chain head header
|
||||||
|
*/
|
||||||
|
public synchronized void startTimer(
|
||||||
|
final ConsensusRoundIdentifier round, final Supplier<Long> headerTimestamp) {
|
||||||
|
cancelTimer();
|
||||||
|
|
||||||
|
final BftConfigOptions currentForkOptions =
|
||||||
|
forksSchedule.getFork(round.getSequenceNumber(), headerTimestamp.get()).getValue();
|
||||||
|
|
||||||
|
final long expiryTime;
|
||||||
|
int currentBlockPeriodSeconds = currentForkOptions.getBlockPeriodSeconds();
|
||||||
|
final int nextBlockPeriodSeconds =
|
||||||
|
forksSchedule
|
||||||
|
.getFork(round.getSequenceNumber(), headerTimestamp.get() + currentBlockPeriodSeconds)
|
||||||
|
.getValue()
|
||||||
|
.getBlockPeriodSeconds();
|
||||||
|
|
||||||
|
// If the block period seconds change between the current block and the next one we need to
|
||||||
|
// produce this block on the longer of the two values,
|
||||||
|
// otherwise block validation will fail (blocks produced too close together)
|
||||||
|
if (nextBlockPeriodSeconds > currentBlockPeriodSeconds) {
|
||||||
|
currentBlockPeriodSeconds = nextBlockPeriodSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Experimental option for test scenarios only. Not for production use.
|
||||||
|
final long blockPeriodMilliseconds = currentForkOptions.getBlockPeriodMilliseconds();
|
||||||
|
if (blockPeriodMilliseconds > 0) {
|
||||||
|
// Experimental mode for setting < 1 second block periods e.g. for CI/CD pipelines
|
||||||
|
// running tests against Besu
|
||||||
|
expiryTime = clock.millis() + blockPeriodMilliseconds;
|
||||||
|
LOG.warn(
|
||||||
|
"Test-mode only xblockperiodmilliseconds has been set to {} millisecond blocks. Do not use in a production system.",
|
||||||
|
blockPeriodMilliseconds);
|
||||||
|
} else {
|
||||||
|
// absolute time when the timer is supposed to expire
|
||||||
|
final long minimumTimeBetweenBlocksMillis = currentBlockPeriodSeconds * 1000L;
|
||||||
|
expiryTime = headerTimestamp.get() * 1_000 + minimumTimeBetweenBlocksMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int emptyBlockPeriodSeconds = currentForkOptions.getEmptyBlockPeriodSeconds();
|
||||||
|
setBlockTimes(currentBlockPeriodSeconds, emptyBlockPeriodSeconds);
|
||||||
|
|
||||||
|
// AERE D-337 (2026-09-04): at an anchor height the proposer needs K seals of every scheme, and
|
||||||
|
// those ride on the commits of the parent. Measured on chain 2800, the timer fires before the
|
||||||
|
// sixth SLH-DSA seal arrives, the producer refuses to write a certificate it cannot fill, and
|
||||||
|
// the anchor pays the whole round-change timeout. The extra wait applies ONLY at anchor heights
|
||||||
|
// and is zero unless configured; see AereAnchorProposalDelay for why this is local timing and
|
||||||
|
// not consensus.
|
||||||
|
startTimer(round, expiryTime + AereAnchorProposalDelay.millisFor(round.getSequenceNumber()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the empty block timer is expired
|
||||||
|
*
|
||||||
|
* @param headerTimestamp Function to get the chain head timestamp
|
||||||
|
* @param currentTimeInMillis The current time
|
||||||
|
* @return a boolean value
|
||||||
|
*/
|
||||||
|
public synchronized boolean checkEmptyBlockExpired(
|
||||||
|
final Supplier<Long> headerTimestamp, final long currentTimeInMillis) {
|
||||||
|
final long emptyBlockPeriodExpiryTime =
|
||||||
|
(headerTimestamp.get() + emptyBlockPeriodSeconds) * 1000;
|
||||||
|
|
||||||
|
if (currentTimeInMillis > emptyBlockPeriodExpiryTime) {
|
||||||
|
LOG.debug("Empty Block expired");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
LOG.debug("Empty Block NOT expired");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the empty block timer
|
||||||
|
*
|
||||||
|
* @param roundIdentifier The current round identifier
|
||||||
|
* @param headerTimestamp Function to get timestamp from the header of the chain head
|
||||||
|
* @param currentTimeInMillis The current time
|
||||||
|
*/
|
||||||
|
public void resetTimerForEmptyBlock(
|
||||||
|
final ConsensusRoundIdentifier roundIdentifier,
|
||||||
|
final Supplier<Long> headerTimestamp,
|
||||||
|
final long currentTimeInMillis) {
|
||||||
|
final long emptyBlockPeriodExpiryTime =
|
||||||
|
(headerTimestamp.get() + emptyBlockPeriodSeconds) * 1000;
|
||||||
|
final long nextBlockPeriodExpiryTime = currentTimeInMillis + blockPeriodSeconds * 1000;
|
||||||
|
|
||||||
|
startTimer(roundIdentifier, Math.min(emptyBlockPeriodExpiryTime, nextBlockPeriodExpiryTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void startTimer(
|
||||||
|
final ConsensusRoundIdentifier round, final long expiryTime) {
|
||||||
|
cancelTimer();
|
||||||
|
final long now = clock.millis();
|
||||||
|
|
||||||
|
if (expiryTime > now) {
|
||||||
|
final long delay = expiryTime - now;
|
||||||
|
|
||||||
|
final Runnable newTimerRunnable = () -> queue.add(new BlockTimerExpiry(round));
|
||||||
|
|
||||||
|
final ScheduledFuture<?> newTimerTask =
|
||||||
|
bftExecutors.scheduleTask(newTimerRunnable, delay, TimeUnit.MILLISECONDS);
|
||||||
|
currentTimerTask = Optional.of(newTimerTask);
|
||||||
|
} else {
|
||||||
|
queue.add(new BlockTimerExpiry(round));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setBlockTimes(
|
||||||
|
final int blockPeriodSeconds, final int emptyBlockPeriodSeconds) {
|
||||||
|
this.blockPeriodSeconds = blockPeriodSeconds;
|
||||||
|
this.emptyBlockPeriodSeconds = emptyBlockPeriodSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the Block Period Seconds
|
||||||
|
*
|
||||||
|
* @return the Block Period Seconds
|
||||||
|
*/
|
||||||
|
public synchronized long getBlockPeriodSeconds() {
|
||||||
|
return blockPeriodSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the Empty Block Period Seconds
|
||||||
|
*
|
||||||
|
* @return the Empty Block Period Seconds
|
||||||
|
*/
|
||||||
|
public synchronized long getEmptyBlockPeriodSeconds() {
|
||||||
|
return emptyBlockPeriodSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -127,7 +127,7 @@ public final class HybridSealSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Drops the cached instance, for tests only. */
|
/** Drops the cached instance, for tests only. */
|
||||||
static void resetForTesting() {
|
public static void resetForTesting() {
|
||||||
instance = null;
|
instance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -185,7 +185,24 @@ public final class HybridSignerRegistry {
|
|||||||
if (perScheme == null) {
|
if (perScheme == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return Optional.ofNullable(perScheme.get(schemeId)).map(byte[]::clone);
|
final byte[] exact = perScheme.get(schemeId);
|
||||||
|
if (exact != null) {
|
||||||
|
return Optional.of(exact.clone());
|
||||||
|
}
|
||||||
|
// D-325 (2026-09-03): a registry written with an ALIAS of a scheme id ("slh-dsa-128s") answers a
|
||||||
|
// lookup by the canonical id ("slh-dsa-sha2-128s") and the other way round. Stored names are
|
||||||
|
// kept as written so the registry's canonical hash does not move under a file that did not.
|
||||||
|
final String wanted = canonicalId(schemeId);
|
||||||
|
for (final Map.Entry<String, byte[]> e : perScheme.entrySet()) {
|
||||||
|
if (canonicalId(e.getKey()).equals(wanted)) {
|
||||||
|
return Optional.of(e.getValue().clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String canonicalId(final String schemeId) {
|
||||||
|
return SealSchemes.byId(schemeId).map(SealScheme::id).orElse(schemeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The 20-byte address bound to {@code index}, or empty. */
|
/** The 20-byte address bound to {@code index}, or empty. */
|
||||||
@ -196,7 +213,11 @@ public final class HybridSignerRegistry {
|
|||||||
/** How many indices hold a key under {@code schemeId}. The arming gate for a scheme asks this:
|
/** How many indices hold a key under {@code schemeId}. The arming gate for a scheme asks this:
|
||||||
* arming a K-of-N threshold under a scheme with coverage below K would be a chain stop. */
|
* arming a K-of-N threshold under a scheme with coverage below K would be a chain stop. */
|
||||||
public int coverage(final String schemeId) {
|
public int coverage(final String schemeId) {
|
||||||
return (int) keys.values().stream().filter(m -> m.containsKey(schemeId)).count();
|
final String wanted = canonicalId(schemeId);
|
||||||
|
return (int)
|
||||||
|
keys.values().stream()
|
||||||
|
.filter(m -> m.keySet().stream().anyMatch(k -> canonicalId(k).equals(wanted)))
|
||||||
|
.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The canonical hash: domain || chainId || count || per index asc: index, addr, schemeCount,
|
/** The canonical hash: domain || chainId || count || per index asc: index, addr, schemeCount,
|
||||||
|
|||||||
@ -205,6 +205,30 @@ public final class PqAnchorConfig {
|
|||||||
/** Environment variable twin of {@link #PROPERTY_ANCHOR_INTERVAL}. */
|
/** Environment variable twin of {@link #PROPERTY_ANCHOR_INTERVAL}. */
|
||||||
public static final String ENV_ANCHOR_INTERVAL = "AERE_PQ_ANCHOR_INTERVAL";
|
public static final String ENV_ANCHOR_INTERVAL = "AERE_PQ_ANCHOR_INTERVAL";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System property naming the INTERVAL SCHEDULE: "h:interval,h:interval". From each height h the
|
||||||
|
* certificate is carried every {@code interval} blocks instead of {@link #PROPERTY_ANCHOR_INTERVAL}.
|
||||||
|
* D-336 (2026-09-04): a hybrid anchor is ~53 KB, so at interval 32 the chain costs ~8.35 GB per
|
||||||
|
* month per node; a scheduled interval lets the fleet move to 128 at a coordinated height.
|
||||||
|
*/
|
||||||
|
public static final String PROPERTY_ANCHOR_INTERVAL_SCHEDULE = "aere.pq.anchorIntervalSchedule";
|
||||||
|
|
||||||
|
/** Environment variable twin of {@link #PROPERTY_ANCHOR_INTERVAL_SCHEDULE}. */
|
||||||
|
public static final String ENV_ANCHOR_INTERVAL_SCHEDULE = "AERE_PQ_ANCHOR_INTERVAL_SCHEDULE";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE ANCHOR V2 (2026-09-03): from this height every anchor header carries the SCHEME-TAGGED
|
||||||
|
* certificate (PqAnchorV2: Falcon-512 plus every extra scheme the scheme schedule names at the
|
||||||
|
* parent height, K seals per scheme) and vanityData is the v2 digest under "AERE-PQ-ANCHOR-2".
|
||||||
|
* Below it nothing changes. A v2 certificate is refused below this height and a v1 one from it,
|
||||||
|
* so the switch is a consensus fork: every node of the fleet must carry the same value, exactly
|
||||||
|
* like {@link #PROPERTY_ANCHOR_BLOCK}. Absent means never. Env: {@code AERE_PQ_ANCHOR_V2_BLOCK}.
|
||||||
|
*/
|
||||||
|
public static final String PROPERTY_ANCHOR_V2_BLOCK = "aere.pq.anchorV2Block";
|
||||||
|
|
||||||
|
/** Environment variable twin of {@link #PROPERTY_ANCHOR_V2_BLOCK}. */
|
||||||
|
public static final String ENV_ANCHOR_V2_BLOCK = "AERE_PQ_ANCHOR_V2_BLOCK";
|
||||||
|
|
||||||
/** Environment variable twin of {@link #PROPERTY_ANCHOR_BLOCK}. */
|
/** Environment variable twin of {@link #PROPERTY_ANCHOR_BLOCK}. */
|
||||||
public static final String ENV_ANCHOR_BLOCK = "AERE_PQ_ANCHOR_BLOCK";
|
public static final String ENV_ANCHOR_BLOCK = "AERE_PQ_ANCHOR_BLOCK";
|
||||||
|
|
||||||
@ -248,6 +272,8 @@ public final class PqAnchorConfig {
|
|||||||
{PROPERTY_MIN_SEALS_CEILING, ENV_MIN_SEALS_CEILING},
|
{PROPERTY_MIN_SEALS_CEILING, ENV_MIN_SEALS_CEILING},
|
||||||
{PROPERTY_MAX_SEALS, ENV_MAX_SEALS},
|
{PROPERTY_MAX_SEALS, ENV_MAX_SEALS},
|
||||||
{PROPERTY_ANCHOR_INTERVAL, ENV_ANCHOR_INTERVAL},
|
{PROPERTY_ANCHOR_INTERVAL, ENV_ANCHOR_INTERVAL},
|
||||||
|
{PROPERTY_ANCHOR_INTERVAL_SCHEDULE, ENV_ANCHOR_INTERVAL_SCHEDULE},
|
||||||
|
{PROPERTY_ANCHOR_V2_BLOCK, ENV_ANCHOR_V2_BLOCK},
|
||||||
{PROPERTY_DISABLE, ENV_DISABLE}
|
{PROPERTY_DISABLE, ENV_DISABLE}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -260,6 +286,8 @@ public final class PqAnchorConfig {
|
|||||||
private final boolean disabled;
|
private final boolean disabled;
|
||||||
private final OptionalInt maxSealsCarried;
|
private final OptionalInt maxSealsCarried;
|
||||||
private final OptionalInt anchorInterval;
|
private final OptionalInt anchorInterval;
|
||||||
|
private final long anchorV2Block;
|
||||||
|
private final NavigableMap<Long, Integer> anchorIntervalSchedule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an explicit configuration.
|
* Build an explicit configuration.
|
||||||
@ -320,7 +348,99 @@ public final class PqAnchorConfig {
|
|||||||
final boolean disabled,
|
final boolean disabled,
|
||||||
final OptionalInt maxSealsCarried,
|
final OptionalInt maxSealsCarried,
|
||||||
final OptionalInt anchorInterval) {
|
final OptionalInt anchorInterval) {
|
||||||
|
this(chainId, anchorBlock, minSealsSchedule, minSealsCeiling, disabled, maxSealsCarried,
|
||||||
|
anchorInterval, NEVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full constructor.
|
||||||
|
*
|
||||||
|
* @param anchorV2Block the height from which anchor headers carry the scheme-tagged v2
|
||||||
|
* certificate and the v2 digest, or {@link #NEVER}
|
||||||
|
*/
|
||||||
|
public PqAnchorConfig(
|
||||||
|
final long chainId,
|
||||||
|
final long anchorBlock,
|
||||||
|
final Map<Long, Integer> minSealsSchedule,
|
||||||
|
final OptionalInt minSealsCeiling,
|
||||||
|
final boolean disabled,
|
||||||
|
final OptionalInt maxSealsCarried,
|
||||||
|
final OptionalInt anchorInterval,
|
||||||
|
final long anchorV2Block) {
|
||||||
|
this(chainId, anchorBlock, minSealsSchedule, minSealsCeiling, disabled, maxSealsCarried,
|
||||||
|
anchorInterval, anchorV2Block, new TreeMap<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full constructor with the interval schedule (D-336).
|
||||||
|
*
|
||||||
|
* @param anchorIntervalSchedule heights from which the interval changes; every interval must be a
|
||||||
|
* positive multiple of the base interval and every height must sit on the NEW grid counted
|
||||||
|
* from H, so that the anchors of the new regime are a subset of the old ones and the change
|
||||||
|
* never invents an anchor height the old regime did not have
|
||||||
|
*/
|
||||||
|
private PqAnchorConfig(
|
||||||
|
final long chainId,
|
||||||
|
final long anchorBlock,
|
||||||
|
final Map<Long, Integer> minSealsSchedule,
|
||||||
|
final OptionalInt minSealsCeiling,
|
||||||
|
final boolean disabled,
|
||||||
|
final OptionalInt maxSealsCarried,
|
||||||
|
final OptionalInt anchorInterval,
|
||||||
|
final long anchorV2Block,
|
||||||
|
final NavigableMap<Long, Integer> anchorIntervalSchedule) {
|
||||||
this.anchorInterval = anchorInterval;
|
this.anchorInterval = anchorInterval;
|
||||||
|
this.anchorV2Block = anchorV2Block;
|
||||||
|
this.anchorIntervalSchedule = new TreeMap<>(anchorIntervalSchedule);
|
||||||
|
if (!this.anchorIntervalSchedule.isEmpty()) {
|
||||||
|
if (anchorInterval.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ANCHOR: " + PROPERTY_ANCHOR_INTERVAL_SCHEDULE + " is set but "
|
||||||
|
+ PROPERTY_ANCHOR_INTERVAL + " is not: a schedule changes an interval, it cannot"
|
||||||
|
+ " create one");
|
||||||
|
}
|
||||||
|
if (anchorBlock == NEVER) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ANCHOR: " + PROPERTY_ANCHOR_INTERVAL_SCHEDULE
|
||||||
|
+ " is set but no anchor activation height is");
|
||||||
|
}
|
||||||
|
final int base = anchorInterval.getAsInt();
|
||||||
|
for (final Map.Entry<Long, Integer> e : this.anchorIntervalSchedule.entrySet()) {
|
||||||
|
final long h = e.getKey();
|
||||||
|
final int iv = e.getValue();
|
||||||
|
if (iv < 1 || iv % base != 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ANCHOR: " + PROPERTY_ANCHOR_INTERVAL_SCHEDULE + " names interval " + iv
|
||||||
|
+ " at " + h + ", which is not a positive multiple of the base interval " + base
|
||||||
|
+ ": the anchors of the new regime must be a subset of the old ones");
|
||||||
|
}
|
||||||
|
if (h < anchorBlock || (h - anchorBlock) % iv != 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ANCHOR: " + PROPERTY_ANCHOR_INTERVAL_SCHEDULE + " changes the interval to "
|
||||||
|
+ iv + " at " + h + ", which is not on the new grid counted from H=" + anchorBlock
|
||||||
|
+ " ((h - H) % " + iv + " must be 0): the first anchor of the new regime is the"
|
||||||
|
+ " activation height itself, so every node can check it by hand");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (anchorV2Block != NEVER) {
|
||||||
|
if (anchorV2Block < 1) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ANCHOR: " + PROPERTY_ANCHOR_V2_BLOCK + " must be at least 1, got " + anchorV2Block);
|
||||||
|
}
|
||||||
|
if (anchorBlock == NEVER) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ANCHOR: " + PROPERTY_ANCHOR_V2_BLOCK + "=" + anchorV2Block
|
||||||
|
+ " is set but no anchor activation height is: a v2 certificate is a form of the"
|
||||||
|
+ " anchor certificate, it cannot exist where no anchor does");
|
||||||
|
}
|
||||||
|
if (anchorV2Block < anchorBlock) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"AERE PQ ANCHOR: " + PROPERTY_ANCHOR_V2_BLOCK + "=" + anchorV2Block
|
||||||
|
+ " is below the anchor activation height " + anchorBlock
|
||||||
|
+ "; the v2 form cannot take effect before the anchor rules do");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (anchorInterval.isPresent() && anchorInterval.getAsInt() < 1) {
|
if (anchorInterval.isPresent() && anchorInterval.getAsInt() < 1) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"AERE PQ ANCHOR: "
|
"AERE PQ ANCHOR: "
|
||||||
@ -550,7 +670,10 @@ public final class PqAnchorConfig {
|
|||||||
|
|
||||||
final PqAnchorConfig bare = strict(block, seals, chain, ceiling, false);
|
final PqAnchorConfig bare = strict(block, seals, chain, ceiling, false);
|
||||||
final PqAnchorConfig config =
|
final PqAnchorConfig config =
|
||||||
bare.withMaxSealsCarried(readMaxSeals()).withAnchorInterval(readAnchorInterval());
|
bare.withMaxSealsCarried(readMaxSeals())
|
||||||
|
.withAnchorInterval(readAnchorInterval())
|
||||||
|
.withAnchorIntervalSchedule(readAnchorIntervalSchedule())
|
||||||
|
.withAnchorV2Block(readAnchorV2Block());
|
||||||
requireMinSealsFloorOrRefuse(config);
|
requireMinSealsFloorOrRefuse(config);
|
||||||
if (config.anchorInterval().isPresent() && config.everActive()) {
|
if (config.anchorInterval().isPresent() && config.everActive()) {
|
||||||
final int iv = config.anchorInterval().getAsInt();
|
final int iv = config.anchorInterval().getAsInt();
|
||||||
@ -565,6 +688,13 @@ public final class PqAnchorConfig {
|
|||||||
iv,
|
iv,
|
||||||
String.format("%.1f", 120.5 / iv));
|
String.format("%.1f", 120.5 / iv));
|
||||||
}
|
}
|
||||||
|
if (!config.anchorIntervalSchedule().isEmpty() && config.everActive()) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR: interval SCHEDULE in force - {} (height:interval). From each height the"
|
||||||
|
+ " certificate is carried every that-many blocks; the anchors of every later regime are"
|
||||||
|
+ " a subset of the earlier ones (D-336, disk).",
|
||||||
|
config.anchorIntervalSchedule());
|
||||||
|
}
|
||||||
if (config.maxSealsCarried().isPresent() && config.everActive()) {
|
if (config.maxSealsCarried().isPresent() && config.everActive()) {
|
||||||
LOG.warn(
|
LOG.warn(
|
||||||
"AERE PQ ANCHOR: proposer-side seal CAP in force - this node writes at most {} seal(s) "
|
"AERE PQ ANCHOR: proposer-side seal CAP in force - this node writes at most {} seal(s) "
|
||||||
@ -787,6 +917,66 @@ public final class PqAnchorConfig {
|
|||||||
* is a refusal, never a silent fallback, because a weakening asked for and quietly dropped and a
|
* is a refusal, never a silent fallback, because a weakening asked for and quietly dropped and a
|
||||||
* weakening quietly APPLIED are both states an operator must never be left guessing between.
|
* weakening quietly APPLIED are both states an operator must never be left guessing between.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Read the v2 activation height. Absent means never; an unreadable value is a refusal, because
|
||||||
|
* this height is a consensus fork and a node guessing it differently from its peers rejects
|
||||||
|
* every anchor header its peers accept, or the other way round.
|
||||||
|
*/
|
||||||
|
private static long readAnchorV2Block() {
|
||||||
|
final Read v2 = read(PROPERTY_ANCHOR_V2_BLOCK, ENV_ANCHOR_V2_BLOCK);
|
||||||
|
if (!v2.hasValue()) {
|
||||||
|
return NEVER;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(v2.raw().trim());
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
REFUSAL_CODE
|
||||||
|
+ ": "
|
||||||
|
+ PROPERTY_ANCHOR_V2_BLOCK
|
||||||
|
+ " is set to '"
|
||||||
|
+ v2.raw()
|
||||||
|
+ "' from "
|
||||||
|
+ v2.source()
|
||||||
|
+ ", which is not a whole number. It is NOT ignored: this height decides which form of"
|
||||||
|
+ " the anchor certificate is valid, and a node guessing it differently from its peers"
|
||||||
|
+ " parts from the chain at the first anchor.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NavigableMap<Long, Integer> readAnchorIntervalSchedule() {
|
||||||
|
final Read sc = read(PROPERTY_ANCHOR_INTERVAL_SCHEDULE, ENV_ANCHOR_INTERVAL_SCHEDULE);
|
||||||
|
final NavigableMap<Long, Integer> out = new TreeMap<>();
|
||||||
|
if (!sc.hasValue()) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (final String part : Splitter.on(',').split(sc.raw())) {
|
||||||
|
final String p = part.trim();
|
||||||
|
if (p.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int colon = p.indexOf(':');
|
||||||
|
try {
|
||||||
|
if (colon < 1) {
|
||||||
|
throw new NumberFormatException(p);
|
||||||
|
}
|
||||||
|
out.put(Long.parseLong(p.substring(0, colon).trim()), Integer.parseInt(p.substring(colon + 1).trim()));
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
REFUSAL_CODE
|
||||||
|
+ ": "
|
||||||
|
+ PROPERTY_ANCHOR_INTERVAL_SCHEDULE
|
||||||
|
+ " is set to '"
|
||||||
|
+ sc.raw()
|
||||||
|
+ "' from "
|
||||||
|
+ sc.source()
|
||||||
|
+ ", which is not a list of height:interval. It is NOT ignored: two nodes that read"
|
||||||
|
+ " it differently disagree about which headers carry a certificate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
private static OptionalInt readAnchorInterval() {
|
private static OptionalInt readAnchorInterval() {
|
||||||
final Read iv = read(PROPERTY_ANCHOR_INTERVAL, ENV_ANCHOR_INTERVAL);
|
final Read iv = read(PROPERTY_ANCHOR_INTERVAL, ENV_ANCHOR_INTERVAL);
|
||||||
if (!iv.hasValue()) {
|
if (!iv.hasValue()) {
|
||||||
@ -1173,7 +1363,8 @@ public final class PqAnchorConfig {
|
|||||||
// CAREFUL: the interval is carried across too. A copy method that drops a field along the way
|
// CAREFUL: the interval is carried across too. A copy method that drops a field along the way
|
||||||
// is exactly how a disarm or an interval would vanish silently when the other control is set.
|
// is exactly how a disarm or an interval would vanish silently when the other control is set.
|
||||||
return new PqAnchorConfig(
|
return new PqAnchorConfig(
|
||||||
chainId, anchorBlock, minSealsSchedule, minSealsCeiling, disabled, cap, anchorInterval);
|
chainId, anchorBlock, minSealsSchedule, minSealsCeiling, disabled, cap, anchorInterval,
|
||||||
|
anchorV2Block);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1186,6 +1377,43 @@ public final class PqAnchorConfig {
|
|||||||
return anchorInterval;
|
return anchorInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interval in force at a height: the base interval, or the last scheduled change at or below
|
||||||
|
* the height (D-336).
|
||||||
|
*
|
||||||
|
* @param blockNumber the height
|
||||||
|
* @return the interval in force there; 1 when no interval is configured
|
||||||
|
*/
|
||||||
|
public int intervalAt(final long blockNumber) {
|
||||||
|
if (anchorInterval.isEmpty()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
final Map.Entry<Long, Integer> e = anchorIntervalSchedule.floorEntry(blockNumber);
|
||||||
|
return e == null ? anchorInterval.getAsInt() : e.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The interval schedule (D-336), possibly empty.
|
||||||
|
*
|
||||||
|
* @return an unmodifiable view, height to interval
|
||||||
|
*/
|
||||||
|
public NavigableMap<Long, Integer> anchorIntervalSchedule() {
|
||||||
|
return java.util.Collections.unmodifiableNavigableMap(anchorIntervalSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a copy carrying an interval schedule (D-336). Refused unless every entry keeps the new
|
||||||
|
* anchors a subset of the old ones; see the private constructor.
|
||||||
|
*
|
||||||
|
* @param schedule heights to intervals
|
||||||
|
* @return a copy of this configuration carrying the schedule
|
||||||
|
*/
|
||||||
|
public PqAnchorConfig withAnchorIntervalSchedule(final Map<Long, Integer> schedule) {
|
||||||
|
return new PqAnchorConfig(
|
||||||
|
chainId, anchorBlock, minSealsSchedule, minSealsCeiling, disabled, maxSealsCarried,
|
||||||
|
anchorInterval, anchorV2Block, new TreeMap<>(schedule));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is this a height at which a certificate is carried and demanded?
|
* Is this a height at which a certificate is carried and demanded?
|
||||||
*
|
*
|
||||||
@ -1203,7 +1431,7 @@ public final class PqAnchorConfig {
|
|||||||
if (anchorBlock == NEVER || blockNumber < anchorBlock) {
|
if (anchorBlock == NEVER || blockNumber < anchorBlock) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return (blockNumber - anchorBlock) % anchorInterval.getAsInt() == 0;
|
return (blockNumber - anchorBlock) % intervalAt(blockNumber) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1222,6 +1450,39 @@ public final class PqAnchorConfig {
|
|||||||
return activeAt(blockNumber) && isAnchorHeight(blockNumber);
|
return activeAt(blockNumber) && isAnchorHeight(blockNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The v2 activation height, or {@link #NEVER}.
|
||||||
|
*
|
||||||
|
* @return the height from which anchor headers carry the scheme-tagged certificate
|
||||||
|
*/
|
||||||
|
public long anchorV2Block() {
|
||||||
|
return anchorV2Block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the header at this height must carry the SCHEME-TAGGED (v2) certificate and the v2
|
||||||
|
* digest. True only at an anchor height ({@link #anchorAppliesAt}) at or above the v2 activation
|
||||||
|
* height. The producer and both rules ask THIS question, not one that resembles it.
|
||||||
|
*
|
||||||
|
* @param blockNumber the height
|
||||||
|
* @return true when the v2 form is the only accepted form at this height
|
||||||
|
*/
|
||||||
|
public boolean anchorV2AppliesAt(final long blockNumber) {
|
||||||
|
return anchorV2Block != NEVER && blockNumber >= anchorV2Block && anchorAppliesAt(blockNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy with a v2 activation height.
|
||||||
|
*
|
||||||
|
* @param v2Block the height, or {@link #NEVER}
|
||||||
|
* @return a configuration identical to this one except for the v2 height
|
||||||
|
*/
|
||||||
|
public PqAnchorConfig withAnchorV2Block(final long v2Block) {
|
||||||
|
return new PqAnchorConfig(
|
||||||
|
chainId, anchorBlock, minSealsSchedule, minSealsCeiling, disabled, maxSealsCarried,
|
||||||
|
anchorInterval, v2Block, anchorIntervalSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a copy carrying an anchor interval.
|
* Return a copy carrying an anchor interval.
|
||||||
*
|
*
|
||||||
@ -1237,7 +1498,8 @@ public final class PqAnchorConfig {
|
|||||||
*/
|
*/
|
||||||
public PqAnchorConfig withAnchorInterval(final OptionalInt interval) {
|
public PqAnchorConfig withAnchorInterval(final OptionalInt interval) {
|
||||||
return new PqAnchorConfig(
|
return new PqAnchorConfig(
|
||||||
chainId, anchorBlock, minSealsSchedule, minSealsCeiling, disabled, maxSealsCarried, interval);
|
chainId, anchorBlock, minSealsSchedule, minSealsCeiling, disabled, maxSealsCarried, interval,
|
||||||
|
anchorV2Block, anchorIntervalSchedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -167,11 +167,27 @@ public final class PqSealCache {
|
|||||||
if (file == null) {
|
if (file == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
final List<FalconSeal> verified;
|
List<FalconSeal> verified;
|
||||||
try {
|
try {
|
||||||
verified =
|
verified =
|
||||||
PqSealStore.readVerified(
|
PqSealStore.readVerified(
|
||||||
file, persistenceChainId, blockNumber, onchainBlockHash, registry);
|
file, persistenceChainId, blockNumber, onchainBlockHash, registry);
|
||||||
|
if (verified.isEmpty()) {
|
||||||
|
// MAGAZIA TINE VARFUL (D-330): the main file may hold a later height that never imported;
|
||||||
|
// the previous slot may hold exactly the head. Same verification, same registry.
|
||||||
|
final Path previous = file.resolveSibling(PqSealStore.PREVIOUS_FILE_NAME);
|
||||||
|
verified =
|
||||||
|
PqSealStore.readVerified(
|
||||||
|
previous, persistenceChainId, blockNumber, onchainBlockHash, registry);
|
||||||
|
if (!verified.isEmpty()) {
|
||||||
|
LOG.info(
|
||||||
|
"AERE PERSISTENTA-SIGILII: {} seal(s) for the chain head {} restored from the PREVIOUS slot {}"
|
||||||
|
+ " (the main file held a later height).",
|
||||||
|
verified.size(),
|
||||||
|
blockNumber,
|
||||||
|
previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (final RuntimeException e) {
|
} catch (final RuntimeException e) {
|
||||||
// readVerified is written not to throw; this is the belt on top of the braces, because an
|
// readVerified is written not to throw; this is the belt on top of the braces, because an
|
||||||
// exception escaping here would turn a lost cache into a node that refuses to start.
|
// exception escaping here would turn a lost cache into a node that refuses to start.
|
||||||
@ -253,6 +269,15 @@ public final class PqSealCache {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// MAGAZIA TINE VARFUL (D-330, 2026-09-03): before the main file moves to a HIGHER height, what it
|
||||||
|
// held becomes the previous slot. The main file may hold a proposal that never imports; the
|
||||||
|
// previous slot then still holds the head, and a restart can restore the head's seals from it.
|
||||||
|
if (persistedBlock >= 0 && blockNumber > persistedBlock && java.nio.file.Files.isRegularFile(file)) {
|
||||||
|
java.nio.file.Files.move(
|
||||||
|
file,
|
||||||
|
file.resolveSibling(PqSealStore.PREVIOUS_FILE_NAME),
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
PqSealStore.writeAtomically(
|
PqSealStore.writeAtomically(
|
||||||
file,
|
file,
|
||||||
PqSealStore.encode(
|
PqSealStore.encode(
|
||||||
@ -281,6 +306,60 @@ public final class PqSealCache {
|
|||||||
* @param onchainBlockHash the ON-CHAIN hash of that block
|
* @param onchainBlockHash the ON-CHAIN hash of that block
|
||||||
* @return the seals, sorted by strictly increasing validator index, possibly empty
|
* @return the seals, sorted by strictly increasing validator index, possibly empty
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* AERE ANCHOR V2: remember the extra (non-Falcon) scheme seals heard on the commits of a block.
|
||||||
|
* Same key as {@link #record}, same window, same idempotence (first seal per validator and
|
||||||
|
* scheme wins). NOT persisted across a restart: a node that restarts holds no extras until it
|
||||||
|
* takes part in one commit, exactly the Falcon situation before PqSealStore, and the producer
|
||||||
|
* then refuses the v2 certificate for one proposer turn rather than writing a short one.
|
||||||
|
*
|
||||||
|
* @param blockNumber the height of the block the seals are over
|
||||||
|
* @param onchainBlockHash the round-independent on-chain hash of that block
|
||||||
|
* @param extraSeals the heard scheme seals, unverified
|
||||||
|
*/
|
||||||
|
public void recordExtras(
|
||||||
|
final long blockNumber, final Hash onchainBlockHash, final Collection<SchemeSeal> extraSeals) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (onchainBlockHash == null || extraSeals == null || extraSeals.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Entry entry =
|
||||||
|
byHash.computeIfAbsent(onchainBlockHash, unused -> new Entry(blockNumber));
|
||||||
|
for (final SchemeSeal seal : extraSeals) {
|
||||||
|
if (seal == null
|
||||||
|
|| seal.getValidatorIndex() < 0
|
||||||
|
|| seal.getSignature() == null
|
||||||
|
|| seal.getSchemeWireId() == SealSchemes.FALCON_512.wireId()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final long key = ((long) seal.getValidatorIndex() << 8) | (seal.getSchemeWireId() & 0xffL);
|
||||||
|
entry.extras.putIfAbsent(key, seal);
|
||||||
|
}
|
||||||
|
if (blockNumber > highestSeen) {
|
||||||
|
highestSeen = blockNumber;
|
||||||
|
}
|
||||||
|
prune();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE ANCHOR V2: the extra scheme seals heard for a block, canonical order, unverified.
|
||||||
|
*
|
||||||
|
* @param blockNumber the height
|
||||||
|
* @param onchainBlockHash the round-independent on-chain hash
|
||||||
|
* @return the seals, or empty when nothing was heard under that key
|
||||||
|
*/
|
||||||
|
public synchronized List<SchemeSeal> extrasFor(
|
||||||
|
final long blockNumber, final Hash onchainBlockHash) {
|
||||||
|
final Entry entry = byHash.get(onchainBlockHash);
|
||||||
|
if (entry == null || entry.blockNumber != blockNumber) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
final List<SchemeSeal> out = new ArrayList<>(entry.extras.values());
|
||||||
|
out.sort(PqAnchorV2.CANONICAL);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized List<FalconSeal> sealsFor(
|
public synchronized List<FalconSeal> sealsFor(
|
||||||
final long blockNumber, final Hash onchainBlockHash) {
|
final long blockNumber, final Hash onchainBlockHash) {
|
||||||
final Entry entry = byHash.get(onchainBlockHash);
|
final Entry entry = byHash.get(onchainBlockHash);
|
||||||
@ -342,6 +421,8 @@ public final class PqSealCache {
|
|||||||
private static final class Entry {
|
private static final class Entry {
|
||||||
private final long blockNumber;
|
private final long blockNumber;
|
||||||
private final Map<Integer, FalconSeal> seals = new LinkedHashMap<>();
|
private final Map<Integer, FalconSeal> seals = new LinkedHashMap<>();
|
||||||
|
/** AERE ANCHOR V2: heard extra (non-Falcon) scheme seals, keyed (index << 8 | wire). */
|
||||||
|
private final Map<Long, SchemeSeal> extras = new LinkedHashMap<>();
|
||||||
|
|
||||||
private Entry(final long blockNumber) {
|
private Entry(final long blockNumber) {
|
||||||
this.blockNumber = blockNumber;
|
this.blockNumber = blockNumber;
|
||||||
|
|||||||
@ -84,6 +84,14 @@ public final class PqSealStore {
|
|||||||
/** The temporary file the atomic write goes through before the rename. */
|
/** The temporary file the atomic write goes through before the rename. */
|
||||||
public static final String TEMP_FILE_NAME = "aere-pq-seals.rlp.tmp";
|
public static final String TEMP_FILE_NAME = "aere-pq-seals.rlp.tmp";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-330 (2026-09-03): the PREVIOUS slot. The main file holds the highest height heard, which can be
|
||||||
|
* a proposal that never imported (testnet 28001: the file held 168032 while the head was 168031,
|
||||||
|
* and after a fleet-wide restart every node had zero seals for the head). The previous slot keeps
|
||||||
|
* what the main file held before its last rotation, so the head's seals survive one extra height.
|
||||||
|
*/
|
||||||
|
public static final String PREVIOUS_FILE_NAME = "aere-pq-seals-anterior.rlp";
|
||||||
|
|
||||||
/** Domain label, so a file written for any other purpose cannot be read as a seal set. */
|
/** Domain label, so a file written for any other purpose cannot be read as a seal set. */
|
||||||
public static final String DOMAIN = "AERE-PQ-SEALSTORE-1";
|
public static final String DOMAIN = "AERE-PQ-SEALSTORE-1";
|
||||||
|
|
||||||
@ -119,6 +127,16 @@ public final class PqSealStore {
|
|||||||
return dataDirectory.resolve(FILE_NAME);
|
return dataDirectory.resolve(FILE_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The previous-slot file beside {@link #fileIn}.
|
||||||
|
*
|
||||||
|
* @param dataDirectory the node data directory
|
||||||
|
* @return the previous slot's path
|
||||||
|
*/
|
||||||
|
public static Path previousFileIn(final Path dataDirectory) {
|
||||||
|
return dataDirectory.resolve(PREVIOUS_FILE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The canonical bytes of a seal set.
|
* The canonical bytes of a seal set.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -32,8 +32,11 @@ public final class SealSchemes {
|
|||||||
if (id == null) {
|
if (id == null) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
// D-325: the SLH-DSA scheme is named "slh-dsa-sha2-128s" on both clients now; the old short id
|
||||||
|
// is still understood so an existing schedule string keeps meaning the same mathematics.
|
||||||
|
final String wanted = SlhDsaSealScheme.LEGACY_ID.equals(id) ? SLH_DSA_128S.id() : id;
|
||||||
for (final SealScheme s : ALL) {
|
for (final SealScheme s : ALL) {
|
||||||
if (s.id().equals(id)) {
|
if (s.id().equals(wanted)) {
|
||||||
return Optional.of(s);
|
return Optional.of(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,14 +13,22 @@ import java.security.SecureRandom;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||||
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSAKeyGenerationParameters;
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAKeyGenerationParameters;
|
||||||
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSAKeyPairGenerator;
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAKeyPairGenerator;
|
||||||
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSAParameters;
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAParameters;
|
||||||
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPrivateKeyParameters;
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAPrivateKeyParameters;
|
||||||
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPublicKeyParameters;
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAPublicKeyParameters;
|
||||||
import org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner;
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSASigner;
|
||||||
|
|
||||||
/** SLH-DSA-128s (small, SHA2 family) as a pluggable seal scheme. */
|
/** SLH-DSA-128s (small, SHA2 family) as a pluggable seal scheme. */
|
||||||
|
/*
|
||||||
|
* D-337 (2026-09-04): the SLH-DSA classes come from the in-tree copy of Bouncy Castle's engine
|
||||||
|
* (package ..bft.slhdsa) whose SHA-2 runs on the JDK's SHA-NI intrinsics. Bouncy Castle's pure-Java
|
||||||
|
* digest made one sha2_128s signature cost 1.4-2.7 s on the validators' CPUs, on the QBFT thread, at
|
||||||
|
* every anchor parent, and the chain slowed from 0.56 to 0.76 s per block. Measured on the same CPUs
|
||||||
|
* the in-tree engine signs in 0.3-0.7 s; same keys, same bytes (SlhDsaFastEngineTest pins that
|
||||||
|
* against the original).
|
||||||
|
*/
|
||||||
public final class SlhDsaSealScheme implements SealScheme {
|
public final class SlhDsaSealScheme implements SealScheme {
|
||||||
|
|
||||||
/** Registry form: the encoded SLH-DSA-128s public key (PK.seed || PK.root), 32 bytes. */
|
/** Registry form: the encoded SLH-DSA-128s public key (PK.seed || PK.root), 32 bytes. */
|
||||||
@ -32,11 +40,16 @@ public final class SlhDsaSealScheme implements SealScheme {
|
|||||||
|
|
||||||
private record Priv(SLHDSAPrivateKeyParameters params) implements PrivateHandle {}
|
private record Priv(SLHDSAPrivateKeyParameters params) implements PrivateHandle {}
|
||||||
|
|
||||||
|
// D-325 (2026-09-03): the id the SECOND client and the public verifier use, "slh-dsa-sha2-128s";
|
||||||
|
// the shorter "slh-dsa-128s" stays accepted as an alias in SealSchemes.byId so no schedule breaks.
|
||||||
@Override
|
@Override
|
||||||
public String id() {
|
public String id() {
|
||||||
return "slh-dsa-128s";
|
return "slh-dsa-sha2-128s";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The alias this build accepted before D-325. */
|
||||||
|
public static final String LEGACY_ID = "slh-dsa-128s";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte wireId() {
|
public byte wireId() {
|
||||||
return 0x02;
|
return 0x02;
|
||||||
|
|||||||
@ -14,15 +14,23 @@
|
|||||||
*/
|
*/
|
||||||
package org.hyperledger.besu.consensus.common.bft.blockcreation;
|
package org.hyperledger.besu.consensus.common.bft.blockcreation;
|
||||||
|
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.hyperledger.besu.datatypes.Hash;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
|
import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.HybridSealSupport;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.HybridSignerRegistry;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchorNotReadyException;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorNotReadyException;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorV2;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqSealCache;
|
import org.hyperledger.besu.consensus.common.bft.PqSealCache;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
|
import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealScheme;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
|
||||||
import org.hyperledger.besu.datatypes.Address;
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
import org.hyperledger.besu.ethereum.ProtocolContext;
|
import org.hyperledger.besu.ethereum.ProtocolContext;
|
||||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||||
@ -31,6 +39,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@ -156,6 +165,51 @@ public final class PqAnchorProducer {
|
|||||||
return cfg.everActive() && blockNumber + 1L >= cfg.anchorBlock();
|
return cfg.everActive() && blockNumber + 1L >= cfg.anchorBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE D-311 (2026-09-02): THE ONE PLACE that says which bytes a commit's post-quantum seal
|
||||||
|
* signs. Both the emitter ({@code QbftRound.pqSealMessageFor}) and the verifier
|
||||||
|
* ({@code MessageValidator.SubsequentMessageValidator}, feeding {@code PqCommitEnforcement})
|
||||||
|
* call this and nothing else.
|
||||||
|
*
|
||||||
|
* <p>Why it exists, measured on the public testnet 28001 the day it was born: with the anchor
|
||||||
|
* armed (as on 2800 since 13,014,000) the emitter signed the anchor form, while the commit
|
||||||
|
* enforcement verified over the ECDSA committed-seal hash. Two copies of one rule, and they
|
||||||
|
* drifted: at {@code aere.pq.commitPq.forkBlock} every commit of every validator was refused
|
||||||
|
* ("does NOT verify over the commit digest") and the chain stopped. The rehearsal (F95) never
|
||||||
|
* saw it because its kit runs without the anchor armed. Arming H1 on 2800 with the old
|
||||||
|
* verifier would have stopped the live chain.
|
||||||
|
*
|
||||||
|
* @param blockNumber the height of the block the commit votes for
|
||||||
|
* @param onchainHash the ROUND-INDEPENDENT on-chain hash of that block (round forced to 0)
|
||||||
|
* @param commitHash the ECDSA committed-seal hash (round-specific)
|
||||||
|
* @return the 32 bytes the Falcon seal (and any hybrid extras) must sign at this height
|
||||||
|
*/
|
||||||
|
public static Bytes32 commitSealMessage(
|
||||||
|
final long blockNumber, final Bytes onchainHash, final Hash commitHash) {
|
||||||
|
return commitSealMessage(blockNumber, () -> onchainHash, commitHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as {@link #commitSealMessage(long, Bytes, Hash)}, with the on-chain hash supplied LAZILY:
|
||||||
|
* it is only needed (and only computed) in the anchor form. Below the anchor the ECDSA digest is
|
||||||
|
* the message and no round-0 re-encoding happens, which is also what keeps every upstream test
|
||||||
|
* that mocks {@code QbftBlockInterface} exactly as it was.
|
||||||
|
*
|
||||||
|
* @param blockNumber the height of the block the commit votes for
|
||||||
|
* @param onchainHash supplier of the ROUND-INDEPENDENT on-chain hash (round forced to 0)
|
||||||
|
* @param commitHash the ECDSA committed-seal hash (round-specific)
|
||||||
|
* @return the 32 bytes the Falcon seal (and any hybrid extras) must sign at this height
|
||||||
|
*/
|
||||||
|
public static Bytes32 commitSealMessage(
|
||||||
|
final long blockNumber,
|
||||||
|
final java.util.function.Supplier<Bytes> onchainHash,
|
||||||
|
final Hash commitHash) {
|
||||||
|
if (sealMessageIsAnchorForm(blockNumber)) {
|
||||||
|
return PqAnchor.commitMessage(config().chainId(), blockNumber, onchainHash.get());
|
||||||
|
}
|
||||||
|
return Bytes32.wrap(commitHash.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the extra data the proposer should encode for a block on top of this parent.
|
* Return the extra data the proposer should encode for a block on top of this parent.
|
||||||
*
|
*
|
||||||
@ -275,6 +329,9 @@ public final class PqAnchorProducer {
|
|||||||
+ "refusing to write it. This is a bug in the producer, not a configuration fault.");
|
+ "refusing to write it. This is a bug in the producer, not a configuration fault.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cfg.anchorV2AppliesAt(blockNumber)) {
|
||||||
|
return applyV2(cfg, blockNumber, parentHeader, base, certificate, message, required, sealCap);
|
||||||
|
}
|
||||||
final Bytes32 digest =
|
final Bytes32 digest =
|
||||||
PqAnchor.anchorDigest(
|
PqAnchor.anchorDigest(
|
||||||
cfg.chainId(), parentNumber, parentHeader.getHash().getBytes(), certificate);
|
cfg.chainId(), parentNumber, parentHeader.getHash().getBytes(), certificate);
|
||||||
@ -296,6 +353,126 @@ public final class PqAnchorProducer {
|
|||||||
certificate);
|
certificate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE ANCHOR V2 (2026-09-03): the SCHEME-TAGGED certificate. Carries the Falcon seals already
|
||||||
|
* verified by the caller, tagged 0x01, plus, for every extra scheme the scheme schedule names at
|
||||||
|
* the PARENT height, the heard seals of that scheme that (a) come from an index that also holds a
|
||||||
|
* Falcon seal in this very certificate, the binding that stops a lone hash-based seal from
|
||||||
|
* standing in for a validator, and (b) verify under the hybrid registry's key for that index and
|
||||||
|
* scheme, over the SAME message M(parent) the Falcon seal signs. K seals per scheme, capped like
|
||||||
|
* Falcon. vanityData becomes the v2 digest under "AERE-PQ-ANCHOR-2". Refuses, through the same
|
||||||
|
* not-ready exception the v1 path uses, rather than writing a certificate short of a scheme.
|
||||||
|
*/
|
||||||
|
private static BftExtraData applyV2(
|
||||||
|
final PqAnchorConfig cfg,
|
||||||
|
final long blockNumber,
|
||||||
|
final BlockHeader parentHeader,
|
||||||
|
final BftExtraData base,
|
||||||
|
final List<FalconSeal> falconCertificate,
|
||||||
|
final Bytes32 message,
|
||||||
|
final int required,
|
||||||
|
final int sealCap) {
|
||||||
|
final long parentNumber = parentHeader.getNumber();
|
||||||
|
final List<SchemeSeal> v2 = new ArrayList<>();
|
||||||
|
final Set<Integer> falconIndices = new LinkedHashSet<>();
|
||||||
|
for (final FalconSeal f : falconCertificate) {
|
||||||
|
v2.add(new SchemeSeal(SealSchemes.FALCON_512.wireId(), f.getValidatorIndex(), f.getSignature()));
|
||||||
|
falconIndices.add(f.getValidatorIndex());
|
||||||
|
}
|
||||||
|
final HybridSealSupport hybrid = HybridSealSupport.instance();
|
||||||
|
final Set<String> schemes =
|
||||||
|
hybrid.schedule().map(sch -> sch.schemesAt(parentNumber)).orElse(Set.of());
|
||||||
|
final List<SchemeSeal> heard =
|
||||||
|
PqSealCache.instance().extrasFor(parentNumber, parentHeader.getHash());
|
||||||
|
for (final String schemeId : schemes) {
|
||||||
|
if (SealSchemes.FALCON_512.id().equals(schemeId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Optional<SealScheme> scheme = SealSchemes.byId(schemeId);
|
||||||
|
if (scheme.isEmpty()) {
|
||||||
|
throw new PqAnchorNotReadyException(
|
||||||
|
parentNumber,
|
||||||
|
0,
|
||||||
|
required,
|
||||||
|
"the scheme schedule names '" + schemeId + "', which this binary does not implement");
|
||||||
|
}
|
||||||
|
final Optional<HybridSignerRegistry> registry = hybrid.registry();
|
||||||
|
if (registry.isEmpty()) {
|
||||||
|
throw new PqAnchorNotReadyException(
|
||||||
|
parentNumber,
|
||||||
|
0,
|
||||||
|
required,
|
||||||
|
"no hybrid registry is loaded on this node, so no " + schemeId + " seal can be verified");
|
||||||
|
}
|
||||||
|
int kept = 0;
|
||||||
|
int rejected = 0;
|
||||||
|
for (final SchemeSeal s : heard) {
|
||||||
|
if (s.getSchemeWireId() != scheme.get().wireId()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!falconIndices.contains(s.getValidatorIndex())) {
|
||||||
|
rejected++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Optional<byte[]> pk = registry.get().publicKey(s.getValidatorIndex(), schemeId);
|
||||||
|
if (pk.isEmpty()
|
||||||
|
|| !scheme.get().verifyRaw(pk.get(), message.toArray(), s.getSignature().toArray())) {
|
||||||
|
rejected++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
v2.add(s);
|
||||||
|
kept++;
|
||||||
|
if (kept >= sealCap) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kept < required) {
|
||||||
|
throw new PqAnchorNotReadyException(
|
||||||
|
parentNumber,
|
||||||
|
kept,
|
||||||
|
required,
|
||||||
|
"V2: heard "
|
||||||
|
+ heard.size()
|
||||||
|
+ " extra seal(s) for parent hash "
|
||||||
|
+ parentHeader.getHash()
|
||||||
|
+ "; "
|
||||||
|
+ kept
|
||||||
|
+ " valid "
|
||||||
|
+ schemeId
|
||||||
|
+ " seal(s) bound to a Falcon-certified index, "
|
||||||
|
+ rejected
|
||||||
|
+ " rejected. The v2 certificate needs K="
|
||||||
|
+ required
|
||||||
|
+ " seals of EVERY scheme the schedule names at height "
|
||||||
|
+ parentNumber
|
||||||
|
+ " ("
|
||||||
|
+ String.join("+", schemes)
|
||||||
|
+ ").");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v2.sort(PqAnchorV2.CANONICAL);
|
||||||
|
final Bytes32 digest =
|
||||||
|
PqAnchorV2.anchorDigestV2(
|
||||||
|
cfg.chainId(), parentNumber, parentHeader.getHash().getBytes(), v2);
|
||||||
|
LOG.debug(
|
||||||
|
"AERE PQ-ANCHOR V2: block {} carries a {}-seal scheme-tagged certificate over parent {} "
|
||||||
|
+ "(K={} per scheme, schemes={}), vanity D2={}",
|
||||||
|
blockNumber,
|
||||||
|
v2.size(),
|
||||||
|
parentNumber,
|
||||||
|
required,
|
||||||
|
schemes,
|
||||||
|
digest);
|
||||||
|
return new BftExtraData(
|
||||||
|
digest,
|
||||||
|
base.getSeals(),
|
||||||
|
base.getVote(),
|
||||||
|
base.getRound(),
|
||||||
|
base.getValidators(),
|
||||||
|
List.of(),
|
||||||
|
v2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The validator set FOR the parent block: the nodes that were entitled to seal it.
|
* The validator set FOR the parent block: the nodes that were entitled to seal it.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
import org.bouncycastle.util.Pack;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class ADRS
|
||||||
|
{
|
||||||
|
static final int WOTS_HASH = 0;
|
||||||
|
static final int WOTS_PK = 1;
|
||||||
|
static final int TREE = 2;
|
||||||
|
static final int FORS_TREE = 3;
|
||||||
|
static final int FORS_PK = 4;
|
||||||
|
static final int WOTS_PRF = 5;
|
||||||
|
static final int FORS_PRF = 6;
|
||||||
|
|
||||||
|
static final int OFFSET_LAYER = 0;
|
||||||
|
static final int OFFSET_TREE = 4;
|
||||||
|
static final int OFFSET_TREE_HGT = 24;
|
||||||
|
static final int OFFSET_TREE_INDEX = 28;
|
||||||
|
static final int OFFSET_TYPE = 16;
|
||||||
|
static final int OFFSET_KP_ADDR = 20;
|
||||||
|
static final int OFFSET_CHAIN_ADDR = 24;
|
||||||
|
static final int OFFSET_HASH_ADDR = 28;
|
||||||
|
|
||||||
|
final byte[] value = new byte[32];
|
||||||
|
|
||||||
|
ADRS()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ADRS(ADRS adrs)
|
||||||
|
{
|
||||||
|
System.arraycopy(adrs.value, 0, this.value, 0, adrs.value.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLayerAddress(int layer)
|
||||||
|
{
|
||||||
|
Pack.intToBigEndian(layer, value, OFFSET_LAYER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLayerAddress()
|
||||||
|
{
|
||||||
|
return Pack.bigEndianToInt(value, OFFSET_LAYER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTreeAddress(long tree)
|
||||||
|
{
|
||||||
|
// tree address is 12 bytes
|
||||||
|
Pack.longToBigEndian(tree, value, OFFSET_TREE + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTreeAddress()
|
||||||
|
{
|
||||||
|
return Pack.bigEndianToLong(value, OFFSET_TREE + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTreeHeight(int height)
|
||||||
|
{
|
||||||
|
Pack.intToBigEndian(height, value, OFFSET_TREE_HGT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTreeIndex(int index)
|
||||||
|
{
|
||||||
|
Pack.intToBigEndian(index, value, OFFSET_TREE_INDEX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTreeIndex()
|
||||||
|
{
|
||||||
|
return Pack.bigEndianToInt(value, OFFSET_TREE_INDEX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// resets part of value to zero in line with 2.7.3
|
||||||
|
public void setTypeAndClear(int type)
|
||||||
|
{
|
||||||
|
Pack.intToBigEndian(type, value, OFFSET_TYPE);
|
||||||
|
|
||||||
|
Arrays.fill(value, 20, value.length, (byte)0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeType(int type)
|
||||||
|
{
|
||||||
|
Pack.intToBigEndian(type, value, OFFSET_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyPairAddress(int keyPairAddr)
|
||||||
|
{
|
||||||
|
Pack.intToBigEndian(keyPairAddr, value, OFFSET_KP_ADDR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getKeyPairAddress()
|
||||||
|
{
|
||||||
|
return Pack.bigEndianToInt(value, OFFSET_KP_ADDR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHashAddress(int hashAddr)
|
||||||
|
{
|
||||||
|
Pack.intToBigEndian(hashAddr, value, OFFSET_HASH_ADDR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChainAddress(int chainAddr)
|
||||||
|
{
|
||||||
|
Pack.intToBigEndian(chainAddr, value, OFFSET_CHAIN_ADDR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class Fors
|
||||||
|
{
|
||||||
|
SLHDSAEngine engine;
|
||||||
|
|
||||||
|
public Fors(SLHDSAEngine engine)
|
||||||
|
{
|
||||||
|
this.engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input: Secret seed SK.seed, start index s, target node height z, public seed PK.seed, address ADRS
|
||||||
|
// Output: n-byte root node - top node on Stack
|
||||||
|
byte[] treehash(byte[] skSeed, int s, int z, byte[] pkSeed, ADRS adrsParam)
|
||||||
|
{
|
||||||
|
if ((s >>> z) << z != s)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedList<NodeEntry> stack = new LinkedList<NodeEntry>();
|
||||||
|
ADRS adrs = new ADRS(adrsParam);
|
||||||
|
|
||||||
|
for (int idx = 0; idx < (1 << z); idx++)
|
||||||
|
{
|
||||||
|
adrs.setTypeAndClear(ADRS.FORS_PRF);
|
||||||
|
adrs.setKeyPairAddress(adrsParam.getKeyPairAddress());
|
||||||
|
adrs.setTreeHeight(0);
|
||||||
|
adrs.setTreeIndex(s + idx);
|
||||||
|
|
||||||
|
byte[] sk = engine.PRF(pkSeed, skSeed, adrs);
|
||||||
|
|
||||||
|
adrs.changeType(ADRS.FORS_TREE);
|
||||||
|
|
||||||
|
byte[] node = engine.F(pkSeed, adrs, sk);
|
||||||
|
|
||||||
|
adrs.setTreeHeight(1);
|
||||||
|
|
||||||
|
int adrsTreeHeight = 1;
|
||||||
|
int adrsTreeIndex = s + idx;
|
||||||
|
|
||||||
|
// while ( Top node on Stack has same height as node )
|
||||||
|
while (!stack.isEmpty() && stack.get(0).nodeHeight == adrsTreeHeight)
|
||||||
|
{
|
||||||
|
adrsTreeIndex = (adrsTreeIndex - 1) / 2;
|
||||||
|
adrs.setTreeIndex(adrsTreeIndex);
|
||||||
|
|
||||||
|
NodeEntry current = stack.remove(0);
|
||||||
|
node = engine.H(pkSeed, adrs, current.nodeValue, node);
|
||||||
|
|
||||||
|
// topmost node is now one layer higher
|
||||||
|
adrs.setTreeHeight(++adrsTreeHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.add(0, new NodeEntry(node, adrsTreeHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack.get(0).nodeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SIG_FORS[] sign(byte[] md, byte[] skSeed, byte[] pkSeed, ADRS paramAdrs)
|
||||||
|
{
|
||||||
|
ADRS adrs = new ADRS(paramAdrs);
|
||||||
|
|
||||||
|
// int[] idxs = message_to_idxs(md, engine.K, engine.A);
|
||||||
|
int[] idxs = base2B(md, engine.A, engine.K);
|
||||||
|
SIG_FORS[] sig_fors = new SIG_FORS[engine.K];
|
||||||
|
// compute signature elements
|
||||||
|
int t = engine.T;
|
||||||
|
for (int i = 0; i < engine.K; i++)
|
||||||
|
{
|
||||||
|
// get next index
|
||||||
|
int idx = idxs[i];
|
||||||
|
// pick private key element
|
||||||
|
adrs.setTypeAndClear(ADRS.FORS_PRF);
|
||||||
|
adrs.setKeyPairAddress(paramAdrs.getKeyPairAddress());
|
||||||
|
adrs.setTreeHeight(0);
|
||||||
|
adrs.setTreeIndex(i * t + idx);
|
||||||
|
|
||||||
|
byte[] sk = engine.PRF(pkSeed, skSeed, adrs);
|
||||||
|
|
||||||
|
adrs.changeType(ADRS.FORS_TREE);
|
||||||
|
|
||||||
|
byte[][] authPath = new byte[engine.A][];
|
||||||
|
// compute auth path
|
||||||
|
for (int j = 0; j < engine.A; j++)
|
||||||
|
{
|
||||||
|
int s = (idx / (1 << j)) ^ 1;
|
||||||
|
authPath[j] = treehash(skSeed, i * t + s * (1 << j), j, pkSeed, adrs);
|
||||||
|
}
|
||||||
|
sig_fors[i] = new SIG_FORS(sk, authPath);
|
||||||
|
}
|
||||||
|
return sig_fors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] pkFromSig(SIG_FORS[] sig_fors, byte[] message, byte[] pkSeed, ADRS adrs)
|
||||||
|
{
|
||||||
|
byte[][] node = new byte[2][];
|
||||||
|
byte[][] root = new byte[engine.K][];
|
||||||
|
int t = engine.T;
|
||||||
|
|
||||||
|
// int[] idxs = message_to_idxs(message, engine.K, engine.A);
|
||||||
|
int[] idxs = base2B(message, engine.A, engine.K);
|
||||||
|
// compute roots
|
||||||
|
for (int i = 0; i < engine.K; i++)
|
||||||
|
{
|
||||||
|
// get next index
|
||||||
|
int idx = idxs[i];
|
||||||
|
// compute leaf
|
||||||
|
byte[] sk = sig_fors[i].getSK();
|
||||||
|
adrs.setTreeHeight(0);
|
||||||
|
adrs.setTreeIndex(i * t + idx);
|
||||||
|
node[0] = engine.F(pkSeed, adrs, sk);
|
||||||
|
// compute root from leaf and AUTH
|
||||||
|
byte[][] authPath = sig_fors[i].getAuthPath();
|
||||||
|
|
||||||
|
adrs.setTreeIndex(i * t + idx);
|
||||||
|
for (int j = 0; j < engine.A; j++)
|
||||||
|
{
|
||||||
|
adrs.setTreeHeight(j + 1);
|
||||||
|
if (((idx / (1 << j)) % 2) == 0)
|
||||||
|
{
|
||||||
|
adrs.setTreeIndex(adrs.getTreeIndex() / 2);
|
||||||
|
node[1] = engine.H(pkSeed, adrs, node[0], authPath[j]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
adrs.setTreeIndex((adrs.getTreeIndex() - 1) / 2);
|
||||||
|
node[1] = engine.H(pkSeed, adrs, authPath[j], node[0]);
|
||||||
|
}
|
||||||
|
node[0] = node[1];
|
||||||
|
}
|
||||||
|
root[i] = node[0];
|
||||||
|
}
|
||||||
|
ADRS forspkADRS = new ADRS(adrs); // copy address to create FTS public key address
|
||||||
|
forspkADRS.setTypeAndClear(ADRS.FORS_PK);
|
||||||
|
forspkADRS.setKeyPairAddress(adrs.getKeyPairAddress());
|
||||||
|
return engine.T_l(pkSeed, forspkADRS, Arrays.concatenate(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int[] base2B(byte[] msg, int b, int outLen)
|
||||||
|
{
|
||||||
|
int[] baseB = new int[outLen];
|
||||||
|
int i = 0;
|
||||||
|
int bits = 0;
|
||||||
|
BigInteger total = BigInteger.ZERO;
|
||||||
|
|
||||||
|
for (int o = 0; o < outLen; o++)
|
||||||
|
{
|
||||||
|
while (bits < b)
|
||||||
|
{
|
||||||
|
total = total.shiftLeft(8).add(BigInteger.valueOf(msg[i] & 0xff));
|
||||||
|
i+= 1;
|
||||||
|
bits += 8;
|
||||||
|
}
|
||||||
|
bits -= b;
|
||||||
|
baseB[o] = (total.shiftRight(bits).mod(BigInteger.valueOf(2).pow(b))).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseB;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class HT
|
||||||
|
{
|
||||||
|
private final byte[] skSeed;
|
||||||
|
private final byte[] pkSeed;
|
||||||
|
SLHDSAEngine engine;
|
||||||
|
WotsPlus wots;
|
||||||
|
|
||||||
|
final byte[] htPubKey;
|
||||||
|
|
||||||
|
public HT(SLHDSAEngine engine, byte[] skSeed, byte[] pkSeed)
|
||||||
|
{
|
||||||
|
this.skSeed = skSeed;
|
||||||
|
this.pkSeed = pkSeed;
|
||||||
|
|
||||||
|
this.engine = engine;
|
||||||
|
this.wots = new WotsPlus(engine);
|
||||||
|
|
||||||
|
ADRS adrs = new ADRS();
|
||||||
|
adrs.setLayerAddress(engine.D - 1);
|
||||||
|
adrs.setTreeAddress(0);
|
||||||
|
|
||||||
|
if (skSeed != null)
|
||||||
|
{
|
||||||
|
htPubKey = xmss_PKgen(skSeed, pkSeed, adrs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
htPubKey = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] sign(byte[] M, long idx_tree, int idx_leaf)
|
||||||
|
{
|
||||||
|
// init
|
||||||
|
ADRS adrs = new ADRS();
|
||||||
|
// sign
|
||||||
|
// adrs.setType(ADRS.TREE);
|
||||||
|
adrs.setLayerAddress(0);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
SIG_XMSS SIG_tmp = xmss_sign(M, skSeed, idx_leaf, pkSeed, adrs);
|
||||||
|
SIG_XMSS[] SIG_HT = new SIG_XMSS[engine.D];
|
||||||
|
SIG_HT[0] = SIG_tmp;
|
||||||
|
|
||||||
|
adrs.setLayerAddress(0);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
|
||||||
|
byte[] root = xmss_pkFromSig(idx_leaf, SIG_tmp, M, pkSeed, adrs);
|
||||||
|
|
||||||
|
for (int j = 1; j < engine.D; j++)
|
||||||
|
{
|
||||||
|
idx_leaf = (int)(idx_tree & ((1 << engine.H_PRIME) - 1)); // least significant bits of idx_tree;
|
||||||
|
idx_tree >>>= engine.H_PRIME; // most significant bits of idx_tree;
|
||||||
|
adrs.setLayerAddress(j);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
SIG_tmp = xmss_sign(root, skSeed, idx_leaf, pkSeed, adrs);
|
||||||
|
SIG_HT[j] = SIG_tmp;
|
||||||
|
if (j < engine.D - 1)
|
||||||
|
{
|
||||||
|
root = xmss_pkFromSig(idx_leaf, SIG_tmp, root, pkSeed, adrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[][] totSigs = new byte[SIG_HT.length][];
|
||||||
|
for (int i = 0; i != totSigs.length; i++)
|
||||||
|
{
|
||||||
|
totSigs[i] = Arrays.concatenate(SIG_HT[i].sig, Arrays.concatenate(SIG_HT[i].auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.concatenate(totSigs);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] xmss_PKgen(byte[] skSeed, byte[] pkSeed, ADRS adrs)
|
||||||
|
{
|
||||||
|
return treehash(skSeed, 0, engine.H_PRIME, pkSeed, adrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input: index idx, XMSS signature SIG_XMSS = (sig || AUTH), n-byte message M, public seed PK.seed, address ADRS
|
||||||
|
// Output: n-byte root value node[0]
|
||||||
|
byte[] xmss_pkFromSig(int idx, SIG_XMSS sig_xmss, byte[] M, byte[] pkSeed, ADRS paramAdrs)
|
||||||
|
{
|
||||||
|
ADRS adrs = new ADRS(paramAdrs);
|
||||||
|
|
||||||
|
// compute WOTS+ pk from WOTS+ sig
|
||||||
|
adrs.setTypeAndClear(ADRS.WOTS_HASH);
|
||||||
|
adrs.setKeyPairAddress(idx);
|
||||||
|
byte[] sig = sig_xmss.getWOTSSig();
|
||||||
|
byte[][] AUTH = sig_xmss.getXMSSAUTH();
|
||||||
|
|
||||||
|
byte[] node0 = wots.pkFromSig(sig, M, pkSeed, adrs);
|
||||||
|
byte[] node1 = null;
|
||||||
|
|
||||||
|
// compute root from WOTS+ pk and AUTH
|
||||||
|
adrs.setTypeAndClear(ADRS.TREE);
|
||||||
|
adrs.setTreeIndex(idx);
|
||||||
|
for (int k = 0; k < engine.H_PRIME; k++)
|
||||||
|
{
|
||||||
|
adrs.setTreeHeight(k + 1);
|
||||||
|
if (((idx / (1 << k)) % 2) == 0)
|
||||||
|
{
|
||||||
|
adrs.setTreeIndex(adrs.getTreeIndex() / 2);
|
||||||
|
node1 = engine.H(pkSeed, adrs, node0, AUTH[k]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
adrs.setTreeIndex((adrs.getTreeIndex() - 1) / 2);
|
||||||
|
node1 = engine.H(pkSeed, adrs, AUTH[k], node0);
|
||||||
|
}
|
||||||
|
node0 = node1;
|
||||||
|
}
|
||||||
|
return node0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Input: n-byte message M, secret seed SK.seed, index idx, public seed PK.seed,
|
||||||
|
// address ADRS
|
||||||
|
// # Output: XMSS signature SIG_XMSS = (sig || AUTH)
|
||||||
|
SIG_XMSS xmss_sign(byte[] M, byte[] skSeed, int idx, byte[] pkSeed, ADRS paramAdrs)
|
||||||
|
{
|
||||||
|
byte[][] AUTH = new byte[engine.H_PRIME][];
|
||||||
|
|
||||||
|
ADRS adrs = new ADRS(paramAdrs);
|
||||||
|
|
||||||
|
adrs.setTypeAndClear(ADRS.TREE);
|
||||||
|
adrs.setLayerAddress(paramAdrs.getLayerAddress());
|
||||||
|
adrs.setTreeAddress(paramAdrs.getTreeAddress());
|
||||||
|
|
||||||
|
// build authentication path
|
||||||
|
for (int j = 0; j < engine.H_PRIME; j++)
|
||||||
|
{
|
||||||
|
int k = (idx >>> j) ^ 1;
|
||||||
|
AUTH[j] = treehash(skSeed, k << j, j, pkSeed, adrs);
|
||||||
|
}
|
||||||
|
adrs = new ADRS(paramAdrs);
|
||||||
|
adrs.setTypeAndClear(ADRS.WOTS_HASH);
|
||||||
|
adrs.setKeyPairAddress(idx);
|
||||||
|
|
||||||
|
byte[] sig = wots.sign(M, skSeed, pkSeed, adrs);
|
||||||
|
|
||||||
|
return new SIG_XMSS(sig, AUTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input: Secret seed SK.seed, start index s, target node height z, public seed PK.seed, address ADRS
|
||||||
|
// Output: n-byte root node - top node on Stack
|
||||||
|
byte[] treehash(byte[] skSeed, int s, int z, byte[] pkSeed, ADRS adrsParam)
|
||||||
|
{
|
||||||
|
if ((s >>> z) << z != s)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedList<NodeEntry> stack = new LinkedList<NodeEntry>();
|
||||||
|
ADRS adrs = new ADRS(adrsParam);
|
||||||
|
|
||||||
|
for (int idx = 0; idx < (1 << z); idx++)
|
||||||
|
{
|
||||||
|
adrs.setTypeAndClear(ADRS.WOTS_HASH);
|
||||||
|
adrs.setKeyPairAddress(s + idx);
|
||||||
|
byte[] node = wots.pkGen(skSeed, pkSeed, adrs);
|
||||||
|
|
||||||
|
adrs.setTypeAndClear(ADRS.TREE);
|
||||||
|
adrs.setTreeHeight(1);
|
||||||
|
adrs.setTreeIndex(s + idx);
|
||||||
|
|
||||||
|
int adrsTreeHeight = 1;
|
||||||
|
int adrsTreeIndex = s + idx;
|
||||||
|
|
||||||
|
// while ( Top node on Stack has same height as node )
|
||||||
|
while (!stack.isEmpty() && stack.get(0).nodeHeight == adrsTreeHeight)
|
||||||
|
{
|
||||||
|
adrsTreeIndex = (adrsTreeIndex - 1) / 2;
|
||||||
|
adrs.setTreeIndex(adrsTreeIndex);
|
||||||
|
|
||||||
|
NodeEntry current = stack.remove(0);
|
||||||
|
node = engine.H(pkSeed, adrs, current.nodeValue, node);
|
||||||
|
|
||||||
|
// topmost node is now one layer higher
|
||||||
|
adrs.setTreeHeight(++adrsTreeHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.add(0, new NodeEntry(node, adrsTreeHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
return stack.get(0).nodeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// # Input: Message M, signature SIG_HT, public seed PK.seed, tree index idx_tree,
|
||||||
|
// leaf index idx_leaf, HT public key PK_HT.
|
||||||
|
// # Output: Boolean
|
||||||
|
public boolean verify(byte[] M, SIG_XMSS[] sig_ht, byte[] pkSeed, long idx_tree, int idx_leaf, byte[] PK_HT)
|
||||||
|
{
|
||||||
|
// init
|
||||||
|
ADRS adrs = new ADRS();
|
||||||
|
// verify
|
||||||
|
SIG_XMSS SIG_tmp = sig_ht[0];
|
||||||
|
adrs.setLayerAddress(0);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
byte[] node = xmss_pkFromSig(idx_leaf, SIG_tmp, M, pkSeed, adrs);
|
||||||
|
for (int j = 1; j < engine.D; j++)
|
||||||
|
{
|
||||||
|
idx_leaf = (int)(idx_tree & ((1 << engine.H_PRIME) - 1)); // least significant bits of idx_tree;
|
||||||
|
idx_tree >>>= engine.H_PRIME; // most significant bits of idx_tree;
|
||||||
|
SIG_tmp = sig_ht[j];
|
||||||
|
adrs.setLayerAddress(j);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
node = xmss_pkFromSig(idx_leaf, SIG_tmp, node, pkSeed, adrs);
|
||||||
|
}
|
||||||
|
return Arrays.areEqual(PK_HT, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class IndexedDigest
|
||||||
|
{
|
||||||
|
final long idx_tree;
|
||||||
|
final int idx_leaf;
|
||||||
|
final byte[] digest;
|
||||||
|
|
||||||
|
IndexedDigest(long idx_tree, int idx_leaf, byte[] digest)
|
||||||
|
{
|
||||||
|
this.idx_tree = idx_tree;
|
||||||
|
this.idx_leaf = idx_leaf;
|
||||||
|
this.digest = digest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import java.security.DigestException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import org.bouncycastle.crypto.ExtendedDigest;
|
||||||
|
import org.bouncycastle.util.Memoable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Bouncy Castle {@link ExtendedDigest} backed by the JDK's {@link MessageDigest}, so SLH-DSA's
|
||||||
|
* millions of short hashes run on the SHA-NI intrinsics. Measured 2026-09-04 on an EPYC host: 3M
|
||||||
|
* SHA-256 of 64 bytes, Bouncy Castle 2533 ms, JDK 335 ms; one sha2_128s signature 1364 ms before,
|
||||||
|
* 325 ms after. {@link Memoable} is how the SLH-DSA engine keeps the pk-seed prefix state: it is
|
||||||
|
* implemented here with {@link MessageDigest#clone()}, which the SUN provider supports for SHA-2.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
final class JdkDigest implements ExtendedDigest, Memoable {
|
||||||
|
private final String algorithm;
|
||||||
|
private final int blockLength;
|
||||||
|
private MessageDigest md;
|
||||||
|
|
||||||
|
JdkDigest(final String algorithm, final int blockLength) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.blockLength = blockLength;
|
||||||
|
try {
|
||||||
|
this.md = MessageDigest.getInstance(algorithm);
|
||||||
|
} catch (final NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("JDK has no " + algorithm, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JdkDigest(final JdkDigest other) {
|
||||||
|
this.algorithm = other.algorithm;
|
||||||
|
this.blockLength = other.blockLength;
|
||||||
|
this.md = cloneOf(other.md);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MessageDigest cloneOf(final MessageDigest d) {
|
||||||
|
try {
|
||||||
|
return (MessageDigest) d.clone();
|
||||||
|
} catch (final CloneNotSupportedException e) {
|
||||||
|
throw new IllegalStateException(d.getAlgorithm() + " is not cloneable", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAlgorithmName() {
|
||||||
|
return algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDigestSize() {
|
||||||
|
return md.getDigestLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getByteLength() {
|
||||||
|
return blockLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(final byte in) {
|
||||||
|
md.update(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(final byte[] in, final int inOff, final int len) {
|
||||||
|
md.update(in, inOff, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int doFinal(final byte[] out, final int outOff) {
|
||||||
|
try {
|
||||||
|
return md.digest(out, outOff, md.getDigestLength());
|
||||||
|
} catch (final DigestException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
md.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Memoable copy() {
|
||||||
|
return new JdkDigest(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset(final Memoable other) {
|
||||||
|
this.md = cloneOf(((JdkDigest) other).md);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
Licenta sub care ne este ingaduit sa purtam in arborele nostru motorul SLH-DSA generat de
|
||||||
|
genereaza-motorul.py, adica fisierele din
|
||||||
|
consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/slhdsa/.
|
||||||
|
Sursa: bcprov-jdk18on-1.80-sources.jar, sha256 29e8414b7a07060b07222bb786dd594d2e411de0d7723d2fac8adedb0801cbef,
|
||||||
|
fisierul org/bouncycastle/LICENSE.java. Licenta e MIT, compatibila cu Apache-2.0 a lui Besu.
|
||||||
|
|
||||||
|
Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||||
|
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
|
||||||
|
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class NodeEntry
|
||||||
|
{
|
||||||
|
final byte[] nodeValue;
|
||||||
|
final int nodeHeight;
|
||||||
|
|
||||||
|
NodeEntry(byte[] nodeValue, int nodeHeight)
|
||||||
|
{
|
||||||
|
this.nodeValue = nodeValue;
|
||||||
|
this.nodeHeight = nodeHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class PK
|
||||||
|
{
|
||||||
|
final byte[] seed;
|
||||||
|
final byte[] root;
|
||||||
|
|
||||||
|
PK(byte[] seed, byte[] root)
|
||||||
|
{
|
||||||
|
this.seed = seed;
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class SIG
|
||||||
|
{
|
||||||
|
private final byte[] r;
|
||||||
|
private final SIG_FORS[] sig_fors;
|
||||||
|
private final SIG_XMSS[] sig_ht;
|
||||||
|
|
||||||
|
public SIG(int n, int k, int a, int d, int hPrime, int wots_len, byte[] signature)
|
||||||
|
{
|
||||||
|
this.r = new byte[n];
|
||||||
|
System.arraycopy(signature, 0, r, 0, n);
|
||||||
|
|
||||||
|
this.sig_fors = new SIG_FORS[k];
|
||||||
|
int offset = n;
|
||||||
|
for (int i = 0; i != k; i++)
|
||||||
|
{
|
||||||
|
byte[] sk = new byte[n];
|
||||||
|
System.arraycopy(signature, offset, sk, 0, n);
|
||||||
|
offset += n;
|
||||||
|
byte[][] authPath = new byte[a][];
|
||||||
|
for (int j = 0; j != a; j++)
|
||||||
|
{
|
||||||
|
authPath[j] = new byte[n];
|
||||||
|
System.arraycopy(signature, offset, authPath[j], 0, n);
|
||||||
|
offset += n;
|
||||||
|
}
|
||||||
|
sig_fors[i] = new SIG_FORS(sk, authPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
sig_ht = new SIG_XMSS[d];
|
||||||
|
for (int i = 0; i != d; i++)
|
||||||
|
{
|
||||||
|
byte[] sig = new byte[wots_len * n];
|
||||||
|
System.arraycopy(signature, offset, sig, 0, sig.length);
|
||||||
|
offset += sig.length;
|
||||||
|
byte[][] authPath = new byte[hPrime][];
|
||||||
|
for (int j = 0; j != hPrime; j++)
|
||||||
|
{
|
||||||
|
authPath[j] = new byte[n];
|
||||||
|
System.arraycopy(signature, offset, authPath[j], 0, n);
|
||||||
|
offset += n;
|
||||||
|
}
|
||||||
|
sig_ht[i] = new SIG_XMSS(sig, authPath);
|
||||||
|
}
|
||||||
|
if (offset != signature.length)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("signature wrong length");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getR()
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SIG_FORS[] getSIG_FORS()
|
||||||
|
{
|
||||||
|
return sig_fors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SIG_XMSS[] getSIG_HT()
|
||||||
|
{
|
||||||
|
return sig_ht;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class SIG_FORS
|
||||||
|
{
|
||||||
|
final byte[][] authPath;
|
||||||
|
final byte[] sk;
|
||||||
|
|
||||||
|
SIG_FORS(byte[] sk, byte[][] authPath)
|
||||||
|
{
|
||||||
|
this.authPath = authPath;
|
||||||
|
this.sk = sk;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] getSK()
|
||||||
|
{
|
||||||
|
return sk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[][] getAuthPath()
|
||||||
|
{
|
||||||
|
return authPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class SIG_XMSS
|
||||||
|
{
|
||||||
|
final byte[] sig;
|
||||||
|
final byte[][] auth;
|
||||||
|
|
||||||
|
public SIG_XMSS(byte[] sig, byte[][] auth)
|
||||||
|
{
|
||||||
|
this.sig = sig;
|
||||||
|
this.auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getWOTSSig()
|
||||||
|
{
|
||||||
|
return sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[][] getXMSSAUTH()
|
||||||
|
{
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class SK
|
||||||
|
{
|
||||||
|
final byte[] seed;
|
||||||
|
final byte[] prf;
|
||||||
|
|
||||||
|
SK(byte[] seed, byte[] prf)
|
||||||
|
{
|
||||||
|
this.seed = seed;
|
||||||
|
this.prf = prf;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,466 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.Digest;
|
||||||
|
import org.bouncycastle.crypto.Xof;
|
||||||
|
import org.bouncycastle.crypto.digests.SHAKEDigest;
|
||||||
|
import org.bouncycastle.crypto.generators.MGF1BytesGenerator;
|
||||||
|
import org.bouncycastle.crypto.macs.HMac;
|
||||||
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
import org.bouncycastle.crypto.params.MGFParameters;
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
import org.bouncycastle.util.Bytes;
|
||||||
|
import org.bouncycastle.util.Memoable;
|
||||||
|
import org.bouncycastle.util.Pack;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
abstract class SLHDSAEngine
|
||||||
|
{
|
||||||
|
final int N;
|
||||||
|
|
||||||
|
final int WOTS_W;
|
||||||
|
final int WOTS_LOGW;
|
||||||
|
final int WOTS_LEN;
|
||||||
|
final int WOTS_LEN1;
|
||||||
|
final int WOTS_LEN2;
|
||||||
|
|
||||||
|
final int D;
|
||||||
|
final int A; // FORS_HEIGHT
|
||||||
|
final int K; // FORS_TREES
|
||||||
|
final int H; // FULL_HEIGHT
|
||||||
|
final int H_PRIME; // H / D
|
||||||
|
|
||||||
|
final int T; // T = 1 << A
|
||||||
|
|
||||||
|
public SLHDSAEngine(int n, int w, int d, int a, int k, int h)
|
||||||
|
{
|
||||||
|
this.N = n;
|
||||||
|
|
||||||
|
/* SPX_WOTS_LEN2 is floor(log(len_1 * (w - 1)) / log(w)) + 1; we precompute */
|
||||||
|
if (w == 16)
|
||||||
|
{
|
||||||
|
WOTS_LOGW = 4;
|
||||||
|
WOTS_LEN1 = (8 * N / WOTS_LOGW);
|
||||||
|
if (N <= 8)
|
||||||
|
{
|
||||||
|
WOTS_LEN2 = 2;
|
||||||
|
}
|
||||||
|
else if (N <= 136)
|
||||||
|
{
|
||||||
|
WOTS_LEN2 = 3;
|
||||||
|
}
|
||||||
|
else if (N <= 256)
|
||||||
|
{
|
||||||
|
WOTS_LEN2 = 4;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("cannot precompute SPX_WOTS_LEN2 for n outside {2, .., 256}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (w == 256)
|
||||||
|
{
|
||||||
|
WOTS_LOGW = 8;
|
||||||
|
WOTS_LEN1 = (8 * N / WOTS_LOGW);
|
||||||
|
if (N <= 1)
|
||||||
|
{
|
||||||
|
WOTS_LEN2 = 1;
|
||||||
|
}
|
||||||
|
else if (N <= 256)
|
||||||
|
{
|
||||||
|
WOTS_LEN2 = 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("cannot precompute SPX_WOTS_LEN2 for n outside {2, .., 256}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("wots_w assumed 16 or 256");
|
||||||
|
}
|
||||||
|
this.WOTS_W = w;
|
||||||
|
this.WOTS_LEN = WOTS_LEN1 + WOTS_LEN2;
|
||||||
|
|
||||||
|
this.D = d;
|
||||||
|
this.A = a;
|
||||||
|
this.K = k;
|
||||||
|
this.H = h;
|
||||||
|
this.H_PRIME = h / d;
|
||||||
|
this.T = 1 << a;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract void init(byte[] pkSeed);
|
||||||
|
|
||||||
|
abstract byte[] F(byte[] pkSeed, ADRS adrs, byte[] m1);
|
||||||
|
|
||||||
|
abstract byte[] H(byte[] pkSeed, ADRS adrs, byte[] m1, byte[] m2);
|
||||||
|
|
||||||
|
abstract IndexedDigest H_msg(byte[] prf, byte[] pkSeed, byte[] pkRoot, byte[] msgPrefix, byte[] msg);
|
||||||
|
|
||||||
|
abstract byte[] T_l(byte[] pkSeed, ADRS adrs, byte[] m);
|
||||||
|
|
||||||
|
abstract byte[] PRF(byte[] pkSeed, byte[] skSeed, ADRS adrs);
|
||||||
|
|
||||||
|
abstract byte[] PRF_msg(byte[] prf, byte[] randomiser, byte[] msgPrefix, byte[] msg);
|
||||||
|
|
||||||
|
static class Sha2Engine
|
||||||
|
extends SLHDSAEngine
|
||||||
|
{
|
||||||
|
private final HMac treeHMac;
|
||||||
|
private final MGF1BytesGenerator mgf1;
|
||||||
|
private final byte[] hmacBuf;
|
||||||
|
private final Digest msgDigest;
|
||||||
|
private final byte[] msgDigestBuf;
|
||||||
|
private final int bl;
|
||||||
|
private final Digest sha256 = new JdkDigest("SHA-256", 64);
|
||||||
|
private final byte[] sha256Buf = new byte[sha256.getDigestSize()];
|
||||||
|
|
||||||
|
private Memoable msgMemo;
|
||||||
|
private Memoable sha256Memo;
|
||||||
|
|
||||||
|
public Sha2Engine(int n, int w, int d, int a, int k, int h)
|
||||||
|
{
|
||||||
|
super(n, w, d, a, k, h);
|
||||||
|
if (n == 16)
|
||||||
|
{
|
||||||
|
this.msgDigest = new JdkDigest("SHA-256", 64);
|
||||||
|
this.treeHMac = new HMac(new JdkDigest("SHA-256", 64));
|
||||||
|
this.mgf1 = new MGF1BytesGenerator(new JdkDigest("SHA-256", 64));
|
||||||
|
this.bl = 64;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.msgDigest = new JdkDigest("SHA-512", 128);
|
||||||
|
this.treeHMac = new HMac(new JdkDigest("SHA-512", 128));
|
||||||
|
this.mgf1 = new MGF1BytesGenerator(new JdkDigest("SHA-512", 128));
|
||||||
|
this.bl = 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hmacBuf = new byte[treeHMac.getMacSize()];
|
||||||
|
this.msgDigestBuf = new byte[msgDigest.getDigestSize()];
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(byte[] pkSeed)
|
||||||
|
{
|
||||||
|
final byte[] padding = new byte[bl];
|
||||||
|
|
||||||
|
msgDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
msgDigest.update(padding, 0, bl - N); // toByte(0, 64 - n)
|
||||||
|
msgMemo = ((Memoable)msgDigest).copy();
|
||||||
|
|
||||||
|
msgDigest.reset();
|
||||||
|
|
||||||
|
sha256.update(pkSeed, 0, pkSeed.length);
|
||||||
|
sha256.update(padding, 0, 64 - pkSeed.length); // toByte(0, 64 - n)
|
||||||
|
sha256Memo = ((Memoable)sha256).copy();
|
||||||
|
|
||||||
|
sha256.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] F(byte[] pkSeed, ADRS adrs, byte[] m1)
|
||||||
|
{
|
||||||
|
byte[] compressedADRS = compressedADRS(adrs);
|
||||||
|
|
||||||
|
((Memoable)sha256).reset(sha256Memo);
|
||||||
|
|
||||||
|
sha256.update(compressedADRS, 0, compressedADRS.length);
|
||||||
|
sha256.update(m1, 0, m1.length);
|
||||||
|
sha256.doFinal(sha256Buf, 0);
|
||||||
|
|
||||||
|
return Arrays.copyOfRange(sha256Buf, 0, N);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] H(byte[] pkSeed, ADRS adrs, byte[] m1, byte[] m2)
|
||||||
|
{
|
||||||
|
byte[] compressedADRS = compressedADRS(adrs);
|
||||||
|
|
||||||
|
((Memoable)msgDigest).reset(msgMemo);
|
||||||
|
|
||||||
|
msgDigest.update(compressedADRS, 0, compressedADRS.length);
|
||||||
|
|
||||||
|
msgDigest.update(m1, 0, m1.length);
|
||||||
|
msgDigest.update(m2, 0, m2.length);
|
||||||
|
|
||||||
|
msgDigest.doFinal(msgDigestBuf, 0);
|
||||||
|
|
||||||
|
return Arrays.copyOfRange(msgDigestBuf, 0, N);
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedDigest H_msg(byte[] prf, byte[] pkSeed, byte[] pkRoot, byte[] msgPrefix, byte[] msg)
|
||||||
|
{
|
||||||
|
int forsMsgBytes = ((A * K) + 7) / 8;
|
||||||
|
int leafBits = H / D;
|
||||||
|
int treeBits = H - leafBits;
|
||||||
|
int leafBytes = (leafBits + 7) / 8;
|
||||||
|
int treeBytes = (treeBits + 7) / 8;
|
||||||
|
int m = forsMsgBytes + leafBytes + treeBytes;
|
||||||
|
byte[] out = new byte[m];
|
||||||
|
byte[] dig = new byte[msgDigest.getDigestSize()];
|
||||||
|
|
||||||
|
msgDigest.update(prf, 0, prf.length);
|
||||||
|
msgDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
msgDigest.update(pkRoot, 0, pkRoot.length);
|
||||||
|
if (msgPrefix != null)
|
||||||
|
{
|
||||||
|
msgDigest.update(msgPrefix, 0, msgPrefix.length);
|
||||||
|
}
|
||||||
|
msgDigest.update(msg, 0, msg.length);
|
||||||
|
msgDigest.doFinal(dig, 0);
|
||||||
|
|
||||||
|
out = bitmask(Arrays.concatenate(prf, pkSeed, dig), out);
|
||||||
|
|
||||||
|
// tree index
|
||||||
|
// currently, only indexes up to 64 bits are supported
|
||||||
|
byte[] treeIndexBuf = new byte[8];
|
||||||
|
System.arraycopy(out, forsMsgBytes, treeIndexBuf, 8 - treeBytes, treeBytes);
|
||||||
|
long treeIndex = Pack.bigEndianToLong(treeIndexBuf, 0);
|
||||||
|
treeIndex &= (~0L) >>> (64 - treeBits);
|
||||||
|
|
||||||
|
byte[] leafIndexBuf = new byte[4];
|
||||||
|
System.arraycopy(out, forsMsgBytes + treeBytes, leafIndexBuf, 4 - leafBytes, leafBytes);
|
||||||
|
|
||||||
|
int leafIndex = Pack.bigEndianToInt(leafIndexBuf, 0);
|
||||||
|
leafIndex &= (~0) >>> (32 - leafBits);
|
||||||
|
|
||||||
|
return new IndexedDigest(treeIndex, leafIndex, Arrays.copyOfRange(out, 0, forsMsgBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] T_l(byte[] pkSeed, ADRS adrs, byte[] m)
|
||||||
|
{
|
||||||
|
byte[] compressedADRS = compressedADRS(adrs);
|
||||||
|
|
||||||
|
((Memoable)msgDigest).reset(msgMemo);
|
||||||
|
|
||||||
|
msgDigest.update(compressedADRS, 0, compressedADRS.length);
|
||||||
|
msgDigest.update(m, 0, m.length);
|
||||||
|
msgDigest.doFinal(msgDigestBuf, 0);
|
||||||
|
|
||||||
|
return Arrays.copyOfRange(msgDigestBuf, 0, N);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] PRF(byte[] pkSeed, byte[] skSeed, ADRS adrs)
|
||||||
|
{
|
||||||
|
int n = skSeed.length;
|
||||||
|
|
||||||
|
((Memoable)sha256).reset(sha256Memo);
|
||||||
|
|
||||||
|
byte[] compressedADRS = compressedADRS(adrs);
|
||||||
|
|
||||||
|
sha256.update(compressedADRS, 0, compressedADRS.length);
|
||||||
|
sha256.update(skSeed, 0, skSeed.length);
|
||||||
|
sha256.doFinal(sha256Buf, 0);
|
||||||
|
|
||||||
|
return Arrays.copyOfRange(sha256Buf, 0, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] PRF_msg(byte[] prf, byte[] randomiser, byte[] msgPrefix, byte[] msg)
|
||||||
|
{
|
||||||
|
treeHMac.init(new KeyParameter(prf));
|
||||||
|
treeHMac.update(randomiser, 0, randomiser.length);
|
||||||
|
if (msgPrefix != null)
|
||||||
|
{
|
||||||
|
treeHMac.update(msgPrefix, 0, msgPrefix.length);
|
||||||
|
}
|
||||||
|
treeHMac.update(msg, 0, msg.length);
|
||||||
|
treeHMac.doFinal(hmacBuf, 0);
|
||||||
|
|
||||||
|
return Arrays.copyOfRange(hmacBuf, 0, N);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] compressedADRS(ADRS adrs)
|
||||||
|
{
|
||||||
|
byte[] rv = new byte[22];
|
||||||
|
System.arraycopy(adrs.value, ADRS.OFFSET_LAYER + 3, rv, 0, 1); // LSB layer address
|
||||||
|
System.arraycopy(adrs.value, ADRS.OFFSET_TREE + 4, rv, 1, 8); // LS 8 bytes Tree address
|
||||||
|
System.arraycopy(adrs.value, ADRS.OFFSET_TYPE + 3, rv, 9, 1); // LSB type
|
||||||
|
System.arraycopy(adrs.value, 20, rv, 10, 12);
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] bitmask(byte[] key, byte[] m)
|
||||||
|
{
|
||||||
|
byte[] mask = new byte[m.length];
|
||||||
|
mgf1.init(new MGFParameters(key));
|
||||||
|
mgf1.generateBytes(mask, 0, mask.length);
|
||||||
|
Bytes.xorTo(m.length, m, mask);
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] bitmask(byte[] key, byte[] m1, byte[] m2)
|
||||||
|
{
|
||||||
|
byte[] mask = new byte[m1.length + m2.length];
|
||||||
|
mgf1.init(new MGFParameters(key));
|
||||||
|
mgf1.generateBytes(mask, 0, mask.length);
|
||||||
|
Bytes.xorTo(m1.length, m1, mask);
|
||||||
|
Bytes.xorTo(m2.length, m2, 0, mask, m1.length);
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] bitmask256(byte[] key, byte[] m)
|
||||||
|
{
|
||||||
|
byte[] mask = new byte[m.length];
|
||||||
|
MGF1BytesGenerator mgf1 = new MGF1BytesGenerator(new JdkDigest("SHA-256", 64));
|
||||||
|
mgf1.init(new MGFParameters(key));
|
||||||
|
mgf1.generateBytes(mask, 0, mask.length);
|
||||||
|
Bytes.xorTo(m.length, m, mask);
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Shake256Engine
|
||||||
|
extends SLHDSAEngine
|
||||||
|
{
|
||||||
|
private final Xof treeDigest;
|
||||||
|
private final Xof maskDigest;
|
||||||
|
|
||||||
|
public Shake256Engine(int n, int w, int d, int a, int k, int h)
|
||||||
|
{
|
||||||
|
super(n, w, d, a, k, h);
|
||||||
|
|
||||||
|
this.treeDigest = new SHAKEDigest(256);
|
||||||
|
this.maskDigest = new SHAKEDigest(256);
|
||||||
|
}
|
||||||
|
|
||||||
|
void init(byte[] pkSeed)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] F(byte[] pkSeed, ADRS adrs, byte[] m1)
|
||||||
|
{
|
||||||
|
byte[] mTheta = m1;
|
||||||
|
|
||||||
|
byte[] rv = new byte[N];
|
||||||
|
|
||||||
|
treeDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
treeDigest.update(adrs.value, 0, adrs.value.length);
|
||||||
|
treeDigest.update(mTheta, 0, mTheta.length);
|
||||||
|
treeDigest.doFinal(rv, 0, rv.length);
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] H(byte[] pkSeed, ADRS adrs, byte[] m1, byte[] m2)
|
||||||
|
{
|
||||||
|
byte[] rv = new byte[N];
|
||||||
|
|
||||||
|
treeDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
treeDigest.update(adrs.value, 0, adrs.value.length);
|
||||||
|
|
||||||
|
treeDigest.update(m1, 0, m1.length);
|
||||||
|
treeDigest.update(m2, 0, m2.length);
|
||||||
|
|
||||||
|
treeDigest.doFinal(rv, 0, rv.length);
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexedDigest H_msg(byte[] R, byte[] pkSeed, byte[] pkRoot, byte[] msgPrefix, byte[] msg)
|
||||||
|
{
|
||||||
|
int forsMsgBytes = ((A * K) + 7) / 8;
|
||||||
|
int leafBits = H / D;
|
||||||
|
int treeBits = H - leafBits;
|
||||||
|
int leafBytes = (leafBits + 7) / 8;
|
||||||
|
int treeBytes = (treeBits + 7) / 8;
|
||||||
|
int m = forsMsgBytes + leafBytes + treeBytes;
|
||||||
|
byte[] out = new byte[m];
|
||||||
|
|
||||||
|
treeDigest.update(R, 0, R.length);
|
||||||
|
treeDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
treeDigest.update(pkRoot, 0, pkRoot.length);
|
||||||
|
if (msgPrefix != null)
|
||||||
|
{
|
||||||
|
treeDigest.update(msgPrefix, 0, msgPrefix.length);
|
||||||
|
}
|
||||||
|
treeDigest.update(msg, 0, msg.length);
|
||||||
|
treeDigest.doFinal(out, 0, out.length);
|
||||||
|
|
||||||
|
// tree index
|
||||||
|
// currently, only indexes up to 64 bits are supported
|
||||||
|
byte[] treeIndexBuf = new byte[8];
|
||||||
|
System.arraycopy(out, forsMsgBytes, treeIndexBuf, 8 - treeBytes, treeBytes);
|
||||||
|
long treeIndex = Pack.bigEndianToLong(treeIndexBuf, 0);
|
||||||
|
treeIndex &= (~0L) >>> (64 - treeBits);
|
||||||
|
|
||||||
|
byte[] leafIndexBuf = new byte[4];
|
||||||
|
System.arraycopy(out, forsMsgBytes + treeBytes, leafIndexBuf, 4 - leafBytes, leafBytes);
|
||||||
|
|
||||||
|
int leafIndex = Pack.bigEndianToInt(leafIndexBuf, 0);
|
||||||
|
leafIndex &= (~0) >>> (32 - leafBits);
|
||||||
|
|
||||||
|
return new IndexedDigest(treeIndex, leafIndex, Arrays.copyOfRange(out, 0, forsMsgBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] T_l(byte[] pkSeed, ADRS adrs, byte[] m)
|
||||||
|
{
|
||||||
|
byte[] mTheta = m;
|
||||||
|
|
||||||
|
byte[] rv = new byte[N];
|
||||||
|
|
||||||
|
treeDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
treeDigest.update(adrs.value, 0, adrs.value.length);
|
||||||
|
treeDigest.update(mTheta, 0, mTheta.length);
|
||||||
|
treeDigest.doFinal(rv, 0, rv.length);
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] PRF(byte[] pkSeed, byte[] skSeed, ADRS adrs)
|
||||||
|
{
|
||||||
|
treeDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
treeDigest.update(adrs.value, 0, adrs.value.length);
|
||||||
|
treeDigest.update(skSeed, 0, skSeed.length);
|
||||||
|
|
||||||
|
byte[] prf = new byte[N];
|
||||||
|
treeDigest.doFinal(prf, 0, N);
|
||||||
|
return prf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] PRF_msg(byte[] prf, byte[] randomiser, byte[] msgPrefix, byte[] msg)
|
||||||
|
{
|
||||||
|
treeDigest.update(prf, 0, prf.length);
|
||||||
|
treeDigest.update(randomiser, 0, randomiser.length);
|
||||||
|
if (msgPrefix != null)
|
||||||
|
{
|
||||||
|
treeDigest.update(msgPrefix, 0, msgPrefix.length);
|
||||||
|
}
|
||||||
|
treeDigest.update(msg, 0, msg.length);
|
||||||
|
|
||||||
|
byte[] out = new byte[N];
|
||||||
|
treeDigest.doFinal(out, 0, out.length);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] bitmask(byte[] pkSeed, ADRS adrs, byte[] m)
|
||||||
|
{
|
||||||
|
byte[] mask = new byte[m.length];
|
||||||
|
maskDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
maskDigest.update(adrs.value, 0, adrs.value.length);
|
||||||
|
maskDigest.doFinal(mask, 0, mask.length);
|
||||||
|
Bytes.xorTo(m.length, m, mask);
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] bitmask(byte[] pkSeed, ADRS adrs, byte[] m1, byte[] m2)
|
||||||
|
{
|
||||||
|
byte[] mask = new byte[m1.length + m2.length];
|
||||||
|
maskDigest.update(pkSeed, 0, pkSeed.length);
|
||||||
|
maskDigest.update(adrs.value, 0, adrs.value.length);
|
||||||
|
maskDigest.doFinal(mask, 0, mask.length);
|
||||||
|
Bytes.xorTo(m1.length, m1, mask);
|
||||||
|
Bytes.xorTo(m2.length, m2, 0, mask, m1.length);
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
interface SLHDSAEngineProvider
|
||||||
|
{
|
||||||
|
int getN();
|
||||||
|
|
||||||
|
SLHDSAEngine get();
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.KeyGenerationParameters;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class SLHDSAKeyGenerationParameters
|
||||||
|
extends KeyGenerationParameters
|
||||||
|
{
|
||||||
|
private final SLHDSAParameters parameters;
|
||||||
|
|
||||||
|
public SLHDSAKeyGenerationParameters(SecureRandom random, SLHDSAParameters parameters)
|
||||||
|
{
|
||||||
|
super(random, -1);
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
SLHDSAParameters getParameters()
|
||||||
|
{
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||||
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPairGenerator;
|
||||||
|
import org.bouncycastle.crypto.KeyGenerationParameters;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class SLHDSAKeyPairGenerator
|
||||||
|
implements AsymmetricCipherKeyPairGenerator
|
||||||
|
{
|
||||||
|
private SecureRandom random;
|
||||||
|
private SLHDSAParameters parameters;
|
||||||
|
|
||||||
|
public void init(KeyGenerationParameters param)
|
||||||
|
{
|
||||||
|
random = param.getRandom();
|
||||||
|
parameters = ((SLHDSAKeyGenerationParameters)param).getParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsymmetricCipherKeyPair internalGenerateKeyPair(byte[] skSeed, byte[] skPrf, byte[] pkSeed)
|
||||||
|
{
|
||||||
|
return implGenerateKeyPair(parameters.getEngine(), skSeed, skPrf, pkSeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsymmetricCipherKeyPair generateKeyPair()
|
||||||
|
{
|
||||||
|
SLHDSAEngine engine = parameters.getEngine();
|
||||||
|
|
||||||
|
byte[] skSeed = sec_rand(engine.N);
|
||||||
|
byte[] skPrf = sec_rand(engine.N);
|
||||||
|
byte[] pkSeed = sec_rand(engine.N);
|
||||||
|
|
||||||
|
return implGenerateKeyPair(engine, skSeed, skPrf, pkSeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsymmetricCipherKeyPair implGenerateKeyPair(SLHDSAEngine engine, byte[] skSeed, byte[] skPrf, byte[] pkSeed)
|
||||||
|
{
|
||||||
|
SK sk = new SK(skSeed, skPrf);
|
||||||
|
|
||||||
|
engine.init(pkSeed);
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
PK pk = new PK(pkSeed, new HT(engine, sk.seed, pkSeed).htPubKey);
|
||||||
|
|
||||||
|
return new AsymmetricCipherKeyPair(
|
||||||
|
new SLHDSAPublicKeyParameters(parameters, pk),
|
||||||
|
new SLHDSAPrivateKeyParameters(parameters, sk, pk));
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] sec_rand(int n)
|
||||||
|
{
|
||||||
|
byte[] rv = new byte[n];
|
||||||
|
|
||||||
|
random.nextBytes(rv);
|
||||||
|
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class SLHDSAKeyParameters
|
||||||
|
extends AsymmetricKeyParameter
|
||||||
|
{
|
||||||
|
private final SLHDSAParameters parameters;
|
||||||
|
|
||||||
|
protected SLHDSAKeyParameters(boolean isPrivate, SLHDSAParameters parameters)
|
||||||
|
{
|
||||||
|
super(isPrivate);
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SLHDSAParameters getParameters()
|
||||||
|
{
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class SLHDSAParameters
|
||||||
|
{
|
||||||
|
public static final int TYPE_PURE = 0;
|
||||||
|
public static final int TYPE_SHA2_256 = 1;
|
||||||
|
public static final int TYPE_SHA2_512 = 2;
|
||||||
|
public static final int TYPE_SHAKE128 = 3;
|
||||||
|
public static final int TYPE_SHAKE256 = 4;
|
||||||
|
|
||||||
|
// "Pure" SLH-DSA Parameters
|
||||||
|
// SHA-2
|
||||||
|
public static final SLHDSAParameters sha2_128f = new SLHDSAParameters(
|
||||||
|
"sha2-128f", new Sha2EngineProvider(16, 16, 22, 6, 33, 66), TYPE_PURE);
|
||||||
|
public static final SLHDSAParameters sha2_128s = new SLHDSAParameters(
|
||||||
|
"sha2-128s", new Sha2EngineProvider(16, 16, 7, 12, 14, 63), TYPE_PURE);
|
||||||
|
|
||||||
|
public static final SLHDSAParameters sha2_192f = new SLHDSAParameters(
|
||||||
|
"sha2-192f", new Sha2EngineProvider(24, 16, 22, 8, 33, 66), TYPE_PURE);
|
||||||
|
public static final SLHDSAParameters sha2_192s = new SLHDSAParameters(
|
||||||
|
"sha2-192s", new Sha2EngineProvider(24, 16, 7, 14, 17, 63), TYPE_PURE);
|
||||||
|
|
||||||
|
public static final SLHDSAParameters sha2_256f = new SLHDSAParameters(
|
||||||
|
"sha2-256f", new Sha2EngineProvider(32, 16, 17, 9, 35, 68), TYPE_PURE);
|
||||||
|
public static final SLHDSAParameters sha2_256s = new SLHDSAParameters(
|
||||||
|
"sha2-256s", new Sha2EngineProvider(32, 16, 8, 14, 22, 64), TYPE_PURE);
|
||||||
|
|
||||||
|
// SHAKE-256.
|
||||||
|
public static final SLHDSAParameters shake_128f = new SLHDSAParameters(
|
||||||
|
"shake-128f", new Shake256EngineProvider(16, 16, 22, 6, 33, 66), TYPE_PURE);
|
||||||
|
public static final SLHDSAParameters shake_128s = new SLHDSAParameters(
|
||||||
|
"shake-128s", new Shake256EngineProvider(16, 16, 7, 12, 14, 63), TYPE_PURE);
|
||||||
|
|
||||||
|
public static final SLHDSAParameters shake_192f = new SLHDSAParameters(
|
||||||
|
"shake-192f", new Shake256EngineProvider(24, 16, 22, 8, 33, 66), TYPE_PURE);
|
||||||
|
public static final SLHDSAParameters shake_192s = new SLHDSAParameters(
|
||||||
|
"shake-192s", new Shake256EngineProvider(24, 16, 7, 14, 17, 63), TYPE_PURE);
|
||||||
|
|
||||||
|
public static final SLHDSAParameters shake_256f = new SLHDSAParameters(
|
||||||
|
"shake-256f", new Shake256EngineProvider(32, 16, 17, 9, 35, 68), TYPE_PURE);
|
||||||
|
public static final SLHDSAParameters shake_256s = new SLHDSAParameters(
|
||||||
|
"shake-256s", new Shake256EngineProvider(32, 16, 8, 14, 22, 64), TYPE_PURE);
|
||||||
|
|
||||||
|
|
||||||
|
// "Pre-hash" SLH-DSA Parameters
|
||||||
|
// SHA-2
|
||||||
|
public static final SLHDSAParameters sha2_128f_with_sha256 = new SLHDSAParameters(
|
||||||
|
"sha2-128f-with-sha256", new Sha2EngineProvider(16, 16, 22, 6, 33, 66), TYPE_SHA2_256);
|
||||||
|
public static final SLHDSAParameters sha2_128s_with_sha256 = new SLHDSAParameters(
|
||||||
|
"sha2-128s-with-sha256", new Sha2EngineProvider(16, 16, 7, 12, 14, 63), TYPE_SHA2_256);
|
||||||
|
|
||||||
|
public static final SLHDSAParameters sha2_192f_with_sha512 = new SLHDSAParameters(
|
||||||
|
"sha2-192f-with-sha512", new Sha2EngineProvider(24, 16, 22, 8, 33, 66), TYPE_SHA2_512);
|
||||||
|
public static final SLHDSAParameters sha2_192s_with_sha512 = new SLHDSAParameters(
|
||||||
|
"sha2-192s-with-sha512", new Sha2EngineProvider(24, 16, 7, 14, 17, 63), TYPE_SHA2_512);
|
||||||
|
|
||||||
|
public static final SLHDSAParameters sha2_256f_with_sha512 = new SLHDSAParameters(
|
||||||
|
"sha2-256f-with-sha512", new Sha2EngineProvider(32, 16, 17, 9, 35, 68), TYPE_SHA2_512);
|
||||||
|
public static final SLHDSAParameters sha2_256s_with_sha512 = new SLHDSAParameters(
|
||||||
|
"sha2-256s-with-sha512", new Sha2EngineProvider(32, 16, 8, 14, 22, 64), TYPE_SHA2_512);
|
||||||
|
|
||||||
|
// SHAKE-256.
|
||||||
|
public static final SLHDSAParameters shake_128f_with_shake128 = new SLHDSAParameters(
|
||||||
|
"shake-128f-with-shake128", new Shake256EngineProvider(16, 16, 22, 6, 33, 66), TYPE_SHAKE128);
|
||||||
|
public static final SLHDSAParameters shake_128s_with_shake128 = new SLHDSAParameters(
|
||||||
|
"shake-128s-with-shake128", new Shake256EngineProvider(16, 16, 7, 12, 14, 63), TYPE_SHAKE128);
|
||||||
|
|
||||||
|
public static final SLHDSAParameters shake_192f_with_shake256 = new SLHDSAParameters(
|
||||||
|
"shake-192f-with-shake256", new Shake256EngineProvider(24, 16, 22, 8, 33, 66), TYPE_SHAKE256);
|
||||||
|
public static final SLHDSAParameters shake_192s_with_shake256 = new SLHDSAParameters(
|
||||||
|
"shake-192s-with-shake256", new Shake256EngineProvider(24, 16, 7, 14, 17, 63), TYPE_SHAKE256);
|
||||||
|
|
||||||
|
public static final SLHDSAParameters shake_256f_with_shake256 = new SLHDSAParameters(
|
||||||
|
"shake-256f-with-shake256", new Shake256EngineProvider(32, 16, 17, 9, 35, 68), TYPE_SHAKE256);
|
||||||
|
public static final SLHDSAParameters shake_256s_with_shake256 = new SLHDSAParameters(
|
||||||
|
"shake-256s-with-shake256", new Shake256EngineProvider(32, 16, 8, 14, 22, 64), TYPE_SHAKE256);
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final SLHDSAEngineProvider engineProvider;
|
||||||
|
private final int preHashDigest;
|
||||||
|
|
||||||
|
private SLHDSAParameters(String name, SLHDSAEngineProvider engineProvider, int preHashDigest)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
this.engineProvider = engineProvider;
|
||||||
|
this.preHashDigest = preHashDigest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getType()
|
||||||
|
{
|
||||||
|
return preHashDigest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getN()
|
||||||
|
{
|
||||||
|
return engineProvider.getN();
|
||||||
|
}
|
||||||
|
|
||||||
|
SLHDSAEngine getEngine()
|
||||||
|
{
|
||||||
|
return engineProvider.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPreHash()
|
||||||
|
{
|
||||||
|
return preHashDigest != TYPE_PURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Sha2EngineProvider
|
||||||
|
implements SLHDSAEngineProvider
|
||||||
|
{
|
||||||
|
private final int n;
|
||||||
|
private final int w;
|
||||||
|
private final int d;
|
||||||
|
private final int a;
|
||||||
|
private final int k;
|
||||||
|
private final int h;
|
||||||
|
|
||||||
|
public Sha2EngineProvider(int n, int w, int d, int a, int k, int h)
|
||||||
|
{
|
||||||
|
this.n = n;
|
||||||
|
this.w = w;
|
||||||
|
this.d = d;
|
||||||
|
this.a = a;
|
||||||
|
this.k = k;
|
||||||
|
this.h = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getN()
|
||||||
|
{
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SLHDSAEngine get()
|
||||||
|
{
|
||||||
|
return new SLHDSAEngine.Sha2Engine(n, w, d, a, k, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Shake256EngineProvider
|
||||||
|
implements SLHDSAEngineProvider
|
||||||
|
{
|
||||||
|
private final int n;
|
||||||
|
private final int w;
|
||||||
|
private final int d;
|
||||||
|
private final int a;
|
||||||
|
private final int k;
|
||||||
|
private final int h;
|
||||||
|
|
||||||
|
public Shake256EngineProvider(int n, int w, int d, int a, int k, int h)
|
||||||
|
{
|
||||||
|
this.n = n;
|
||||||
|
this.w = w;
|
||||||
|
this.d = d;
|
||||||
|
this.a = a;
|
||||||
|
this.k = k;
|
||||||
|
this.h = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getN()
|
||||||
|
{
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SLHDSAEngine get()
|
||||||
|
{
|
||||||
|
return new SLHDSAEngine.Shake256Engine(n, w, d, a, k, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class SLHDSAPrivateKeyParameters
|
||||||
|
extends SLHDSAKeyParameters
|
||||||
|
{
|
||||||
|
final SK sk;
|
||||||
|
final PK pk;
|
||||||
|
|
||||||
|
public SLHDSAPrivateKeyParameters(SLHDSAParameters parameters, byte[] skpkEncoded)
|
||||||
|
{
|
||||||
|
super(true, parameters);
|
||||||
|
int n = parameters.getN();
|
||||||
|
if (skpkEncoded.length != 4 * n)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("private key encoding does not match parameters");
|
||||||
|
}
|
||||||
|
this.sk = new SK(Arrays.copyOfRange(skpkEncoded, 0, n), Arrays.copyOfRange(skpkEncoded, n, 2 * n));
|
||||||
|
this.pk = new PK(Arrays.copyOfRange(skpkEncoded, 2 * n, 3 * n), Arrays.copyOfRange(skpkEncoded, 3 * n, 4 * n));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SLHDSAPrivateKeyParameters(SLHDSAParameters parameters, byte[] skSeed, byte[] prf, byte[] pkSeed, byte[] pkRoot)
|
||||||
|
{
|
||||||
|
super(true, parameters);
|
||||||
|
this.sk = new SK(skSeed, prf);
|
||||||
|
this.pk = new PK(pkSeed, pkRoot);
|
||||||
|
}
|
||||||
|
SLHDSAPrivateKeyParameters(SLHDSAParameters parameters, SK sk, PK pk)
|
||||||
|
{
|
||||||
|
super(true, parameters);
|
||||||
|
this.sk = sk;
|
||||||
|
this.pk = pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSeed()
|
||||||
|
{
|
||||||
|
return Arrays.clone(sk.seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPrf()
|
||||||
|
{
|
||||||
|
return Arrays.clone(sk.prf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPublicSeed()
|
||||||
|
{
|
||||||
|
return Arrays.clone(pk.seed);
|
||||||
|
}
|
||||||
|
public byte[] getRoot()
|
||||||
|
{
|
||||||
|
return Arrays.clone(pk.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPublicKey()
|
||||||
|
{
|
||||||
|
return Arrays.concatenate(pk.seed, pk.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEncoded()
|
||||||
|
{
|
||||||
|
return Arrays.concatenate(new byte[][]{ sk.seed, sk.prf, pk.seed, pk.root });
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEncodedPublicKey()
|
||||||
|
{
|
||||||
|
return Arrays.concatenate(pk.seed, pk.root);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class SLHDSAPublicKeyParameters
|
||||||
|
extends SLHDSAKeyParameters
|
||||||
|
{
|
||||||
|
private final PK pk;
|
||||||
|
|
||||||
|
public SLHDSAPublicKeyParameters(SLHDSAParameters parameters, byte[] pkValues)
|
||||||
|
{
|
||||||
|
super(false, parameters);
|
||||||
|
int n = parameters.getN();
|
||||||
|
if (pkValues.length != 2 * n)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("public key encoding does not match parameters");
|
||||||
|
}
|
||||||
|
this.pk = new PK(Arrays.copyOfRange(pkValues, 0, n), Arrays.copyOfRange(pkValues, n, 2 * n));
|
||||||
|
}
|
||||||
|
|
||||||
|
SLHDSAPublicKeyParameters(SLHDSAParameters parameters, PK pk)
|
||||||
|
{
|
||||||
|
super(false, parameters);
|
||||||
|
this.pk = pk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSeed()
|
||||||
|
{
|
||||||
|
return Arrays.clone(pk.seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getRoot()
|
||||||
|
{
|
||||||
|
return Arrays.clone(pk.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEncoded()
|
||||||
|
{
|
||||||
|
return Arrays.concatenate(pk.seed, pk.root);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.CipherParameters;
|
||||||
|
import org.bouncycastle.crypto.params.ParametersWithContext;
|
||||||
|
import org.bouncycastle.crypto.params.ParametersWithRandom;
|
||||||
|
import org.bouncycastle.pqc.crypto.MessageSigner;
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SLH-DA signer.
|
||||||
|
* <p>
|
||||||
|
* This version is based on the 3rd submission with deference to the updated reference
|
||||||
|
* implementation on github as at November 9th 2021. This version includes the changes
|
||||||
|
* for the countermeasure for the long-message second preimage attack - see
|
||||||
|
* "https://github.com/sphincs/sphincsplus/commit/61cd2695c6f984b4f4d6ed675378ed9a486cbede"
|
||||||
|
* for further details.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
public class SLHDSASigner
|
||||||
|
implements MessageSigner
|
||||||
|
{
|
||||||
|
private static final byte[] DEFAULT_PREFIX = new byte[]{ 0, 0 };
|
||||||
|
|
||||||
|
private byte[] msgPrefix;
|
||||||
|
private SLHDSAPublicKeyParameters pubKey;
|
||||||
|
private SLHDSAPrivateKeyParameters privKey;
|
||||||
|
private SecureRandom random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base constructor.
|
||||||
|
*/
|
||||||
|
public SLHDSASigner()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(boolean forSigning, CipherParameters param)
|
||||||
|
{
|
||||||
|
if (param instanceof ParametersWithContext)
|
||||||
|
{
|
||||||
|
ParametersWithContext withContext = (ParametersWithContext)param;
|
||||||
|
param = withContext.getParameters();
|
||||||
|
|
||||||
|
int ctxLength = withContext.getContextLength();
|
||||||
|
if (ctxLength > 255)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("context too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
msgPrefix = new byte[2 + ctxLength];
|
||||||
|
msgPrefix[0] = 0;
|
||||||
|
msgPrefix[1] = (byte)ctxLength;
|
||||||
|
withContext.copyContextTo(msgPrefix, 2, ctxLength);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msgPrefix = DEFAULT_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
SLHDSAParameters parameters;
|
||||||
|
if (forSigning)
|
||||||
|
{
|
||||||
|
pubKey = null;
|
||||||
|
|
||||||
|
if (param instanceof ParametersWithRandom)
|
||||||
|
{
|
||||||
|
ParametersWithRandom withRandom = (ParametersWithRandom)param;
|
||||||
|
privKey = (SLHDSAPrivateKeyParameters)withRandom.getParameters();
|
||||||
|
random = withRandom.getRandom();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
privKey = (SLHDSAPrivateKeyParameters)param;
|
||||||
|
random = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters = privKey.getParameters();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pubKey = (SLHDSAPublicKeyParameters)param;
|
||||||
|
privKey = null;
|
||||||
|
random = null;
|
||||||
|
|
||||||
|
parameters = pubKey.getParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameters.isPreHash())
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("\"pure\" slh-dsa must use non pre-hash parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] generateSignature(byte[] message)
|
||||||
|
{
|
||||||
|
// TODO Redundant with the engine created in internalGenerateSignature
|
||||||
|
SLHDSAEngine engine = privKey.getParameters().getEngine();
|
||||||
|
|
||||||
|
engine.init(privKey.pk.seed);
|
||||||
|
|
||||||
|
// generate randomizer
|
||||||
|
byte[] optRand = new byte[engine.N];
|
||||||
|
if (random != null)
|
||||||
|
{
|
||||||
|
random.nextBytes(optRand);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.arraycopy(privKey.pk.seed, 0, optRand, 0, optRand.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return internalGenerateSignature(privKey, msgPrefix, message, optRand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equivalent to slh_verify_internal from specs
|
||||||
|
public boolean verifySignature(byte[] message, byte[] signature)
|
||||||
|
{
|
||||||
|
return internalVerifySignature(pubKey, msgPrefix, message, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean internalVerifySignature(byte[] message, byte[] signature)
|
||||||
|
{
|
||||||
|
return internalVerifySignature(pubKey, null, message, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean internalVerifySignature(SLHDSAPublicKeyParameters pubKey, byte[] msgPrefix, byte[] msg,
|
||||||
|
byte[] signature)
|
||||||
|
{
|
||||||
|
// TODO Check init via pubKey != null
|
||||||
|
|
||||||
|
//# Input: Message M, signature SIG, public key PK
|
||||||
|
//# Output: Boolean
|
||||||
|
|
||||||
|
// init
|
||||||
|
SLHDSAEngine engine = pubKey.getParameters().getEngine();
|
||||||
|
|
||||||
|
engine.init(pubKey.getSeed());
|
||||||
|
|
||||||
|
ADRS adrs = new ADRS();
|
||||||
|
|
||||||
|
if (((1 + engine.K * (1 + engine.A) + engine.H + engine.D * engine.WOTS_LEN) * engine.N) != signature.length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SIG sig = new SIG(engine.N, engine.K, engine.A, engine.D, engine.H_PRIME, engine.WOTS_LEN, signature);
|
||||||
|
|
||||||
|
byte[] R = sig.getR();
|
||||||
|
SIG_FORS[] sig_fors = sig.getSIG_FORS();
|
||||||
|
SIG_XMSS[] SIG_HT = sig.getSIG_HT();
|
||||||
|
|
||||||
|
// compute message digest and index
|
||||||
|
IndexedDigest idxDigest = engine.H_msg(R, pubKey.getSeed(), pubKey.getRoot(), msgPrefix, msg);
|
||||||
|
byte[] mHash = idxDigest.digest;
|
||||||
|
long idx_tree = idxDigest.idx_tree;
|
||||||
|
int idx_leaf = idxDigest.idx_leaf;
|
||||||
|
|
||||||
|
// compute FORS public key
|
||||||
|
adrs.setTypeAndClear(ADRS.FORS_TREE);
|
||||||
|
adrs.setLayerAddress(0);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
adrs.setKeyPairAddress(idx_leaf);
|
||||||
|
byte[] PK_FORS = new Fors(engine).pkFromSig(sig_fors, mHash, pubKey.getSeed(), adrs);
|
||||||
|
// verify HT signature
|
||||||
|
adrs.setTypeAndClear(ADRS.TREE);
|
||||||
|
adrs.setLayerAddress(0);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
adrs.setKeyPairAddress(idx_leaf);
|
||||||
|
HT ht = new HT(engine, null, pubKey.getSeed());
|
||||||
|
return ht.verify(PK_FORS, SIG_HT, pubKey.getSeed(), idx_tree, idx_leaf, pubKey.getRoot());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] internalGenerateSignature(byte[] message, byte[] optRand)
|
||||||
|
{
|
||||||
|
return internalGenerateSignature(privKey, null, message, optRand);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] internalGenerateSignature(SLHDSAPrivateKeyParameters privKey, byte[] msgPrefix, byte[] msg,
|
||||||
|
byte[] optRand)
|
||||||
|
{
|
||||||
|
// TODO Check init via privKey != null
|
||||||
|
|
||||||
|
SLHDSAEngine engine = privKey.getParameters().getEngine();
|
||||||
|
engine.init(privKey.pk.seed);
|
||||||
|
|
||||||
|
Fors fors = new Fors(engine);
|
||||||
|
byte[] R = engine.PRF_msg(privKey.sk.prf, optRand, msgPrefix, msg);
|
||||||
|
|
||||||
|
IndexedDigest idxDigest = engine.H_msg(R, privKey.pk.seed, privKey.pk.root, msgPrefix, msg);
|
||||||
|
byte[] mHash = idxDigest.digest;
|
||||||
|
long idx_tree = idxDigest.idx_tree;
|
||||||
|
int idx_leaf = idxDigest.idx_leaf;
|
||||||
|
// FORS sign
|
||||||
|
ADRS adrs = new ADRS();
|
||||||
|
adrs.setTypeAndClear(ADRS.FORS_TREE);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
adrs.setKeyPairAddress(idx_leaf);
|
||||||
|
SIG_FORS[] sig_fors = fors.sign(mHash, privKey.sk.seed, privKey.pk.seed, adrs);
|
||||||
|
// get FORS public key - spec shows M?
|
||||||
|
adrs = new ADRS();
|
||||||
|
adrs.setTypeAndClear(ADRS.FORS_TREE);
|
||||||
|
adrs.setTreeAddress(idx_tree);
|
||||||
|
adrs.setKeyPairAddress(idx_leaf);
|
||||||
|
byte[] PK_FORS = fors.pkFromSig(sig_fors, mHash, privKey.pk.seed, adrs);
|
||||||
|
|
||||||
|
// sign FORS public key with HT
|
||||||
|
ADRS treeAdrs = new ADRS();
|
||||||
|
treeAdrs.setTypeAndClear(ADRS.TREE);
|
||||||
|
|
||||||
|
HT ht = new HT(engine, privKey.getSeed(), privKey.getPublicSeed());
|
||||||
|
byte[] SIG_HT = ht.sign(PK_FORS, idx_tree, idx_leaf);
|
||||||
|
|
||||||
|
byte[][] sigComponents = new byte[sig_fors.length + 2][];
|
||||||
|
sigComponents[0] = R;
|
||||||
|
|
||||||
|
for (int i = 0; i != sig_fors.length; i++)
|
||||||
|
{
|
||||||
|
sigComponents[1 + i] = Arrays.concatenate(sig_fors[i].sk, Arrays.concatenate(sig_fors[i].authPath));
|
||||||
|
}
|
||||||
|
sigComponents[sigComponents.length - 1] = SIG_HT;
|
||||||
|
|
||||||
|
return Arrays.concatenate(sigComponents);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Bouncy Castle 1.80, package org.bouncycastle.pqc.crypto.slhdsa (MIT licence,
|
||||||
|
* Copyright (c) 2000-2025 The Legion of the Bouncy Castle Inc., see LICENTA-BOUNCYCASTLE.txt),
|
||||||
|
* regenerated into Aere Network by consensus-pqc/slhdsa-fork/genereaza-motorul.py on 2026-09-04 (D-337).
|
||||||
|
* The ONLY change of substance: the SHA-2 engine hashes through the JDK's MessageDigest (SHA-NI
|
||||||
|
* intrinsics) instead of Bouncy Castle's pure-Java digests, which made one SLH-DSA-SHA2-128s signature
|
||||||
|
* cost seconds on the QBFT thread at every anchor parent. Algorithm, key formats and signature bytes are
|
||||||
|
* FIPS 205 as before; SlhDsaFastEngineTest pins that against the original, in both directions.
|
||||||
|
* DO NOT EDIT BY HAND: regenerate with the script above.
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft.slhdsa;
|
||||||
|
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
import org.bouncycastle.util.Pack;
|
||||||
|
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
class WotsPlus
|
||||||
|
{
|
||||||
|
private final SLHDSAEngine engine;
|
||||||
|
private final int w;
|
||||||
|
|
||||||
|
WotsPlus(SLHDSAEngine engine)
|
||||||
|
{
|
||||||
|
this.engine = engine;
|
||||||
|
this.w = this.engine.WOTS_W;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] pkGen(byte[] skSeed, byte[] pkSeed, ADRS paramAdrs)
|
||||||
|
{
|
||||||
|
ADRS wotspkADRS = new ADRS(paramAdrs); // copy address to create OTS public key address
|
||||||
|
|
||||||
|
byte[][] tmp = new byte[engine.WOTS_LEN][];
|
||||||
|
for (int i = 0; i < engine.WOTS_LEN; i++)
|
||||||
|
{
|
||||||
|
ADRS adrs = new ADRS(paramAdrs);
|
||||||
|
adrs.setTypeAndClear(ADRS.WOTS_PRF);
|
||||||
|
adrs.setKeyPairAddress(paramAdrs.getKeyPairAddress());
|
||||||
|
adrs.setChainAddress(i);
|
||||||
|
adrs.setHashAddress(0);
|
||||||
|
|
||||||
|
byte[] sk = engine.PRF(pkSeed, skSeed, adrs);
|
||||||
|
|
||||||
|
adrs.setTypeAndClear(ADRS.WOTS_HASH);
|
||||||
|
adrs.setKeyPairAddress(paramAdrs.getKeyPairAddress());
|
||||||
|
adrs.setChainAddress(i);
|
||||||
|
adrs.setHashAddress(0);
|
||||||
|
tmp[i] = chain(sk, 0, w - 1, pkSeed, adrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
wotspkADRS.setTypeAndClear(ADRS.WOTS_PK);
|
||||||
|
wotspkADRS.setKeyPairAddress(paramAdrs.getKeyPairAddress());
|
||||||
|
|
||||||
|
return engine.T_l(pkSeed, wotspkADRS, Arrays.concatenate(tmp));
|
||||||
|
}
|
||||||
|
|
||||||
|
// #Input: Input string X, start index i, number of steps s, public seed PK.seed, address ADRS
|
||||||
|
// #Output: value of F iterated s times on X
|
||||||
|
byte[] chain(byte[] X, int i, int s, byte[] pkSeed, ADRS adrs)
|
||||||
|
{
|
||||||
|
if (s == 0)
|
||||||
|
{
|
||||||
|
return Arrays.clone(X);
|
||||||
|
}
|
||||||
|
if ((i + s) > (this.w - 1))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] result = X;
|
||||||
|
for (int j = 0; j < s; ++j)
|
||||||
|
{
|
||||||
|
adrs.setHashAddress(i + j);
|
||||||
|
result = engine.F(pkSeed, adrs, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #Input: Message M, secret seed SK.seed, public seed PK.seed, address ADRS
|
||||||
|
// #Output: WOTS+ signature sig
|
||||||
|
public byte[] sign(byte[] M, byte[] skSeed, byte[] pkSeed, ADRS paramAdrs)
|
||||||
|
{
|
||||||
|
ADRS adrs = new ADRS(paramAdrs);
|
||||||
|
|
||||||
|
int[] msg = new int[engine.WOTS_LEN];
|
||||||
|
|
||||||
|
// convert message to base w
|
||||||
|
base_w(M, 0, w, msg, 0, engine.WOTS_LEN1);
|
||||||
|
|
||||||
|
// compute checksum
|
||||||
|
int csum = 0;
|
||||||
|
for (int i = 0; i < engine.WOTS_LEN1; i++)
|
||||||
|
{
|
||||||
|
csum += w - 1 - msg[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert csum to base w
|
||||||
|
if ((engine.WOTS_LOGW % 8) != 0)
|
||||||
|
{
|
||||||
|
csum = csum << (8 - ((engine.WOTS_LEN2 * engine.WOTS_LOGW) % 8));
|
||||||
|
}
|
||||||
|
int len_2_bytes = (engine.WOTS_LEN2 * engine.WOTS_LOGW + 7) / 8;
|
||||||
|
byte[] csum_bytes = Pack.intToBigEndian(csum);
|
||||||
|
base_w(csum_bytes, 4 - len_2_bytes, w, msg, engine.WOTS_LEN1, engine.WOTS_LEN2);
|
||||||
|
|
||||||
|
byte[][] sig = new byte[engine.WOTS_LEN][];
|
||||||
|
for (int i = 0; i < engine.WOTS_LEN; i++)
|
||||||
|
{
|
||||||
|
adrs.setTypeAndClear(ADRS.WOTS_PRF);
|
||||||
|
adrs.setKeyPairAddress(paramAdrs.getKeyPairAddress());
|
||||||
|
adrs.setChainAddress(i);
|
||||||
|
adrs.setHashAddress(0);
|
||||||
|
byte[] sk = engine.PRF(pkSeed, skSeed, adrs);
|
||||||
|
adrs.setTypeAndClear(ADRS.WOTS_HASH);
|
||||||
|
adrs.setKeyPairAddress(paramAdrs.getKeyPairAddress());
|
||||||
|
adrs.setChainAddress(i);
|
||||||
|
adrs.setHashAddress(0);
|
||||||
|
sig[i] = chain(sk, 0, msg[i], pkSeed, adrs);
|
||||||
|
}
|
||||||
|
return Arrays.concatenate(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Input: len_X-byte string X, int w, output length out_len
|
||||||
|
// Output: out_len int array basew
|
||||||
|
void base_w(byte[] X, int XOff, int w, int[] output, int outOff, int outLen)
|
||||||
|
{
|
||||||
|
int total = 0;
|
||||||
|
int bits = 0;
|
||||||
|
|
||||||
|
for (int consumed = 0; consumed < outLen; consumed++)
|
||||||
|
{
|
||||||
|
if (bits == 0)
|
||||||
|
{
|
||||||
|
total = X[XOff++];
|
||||||
|
bits += 8;
|
||||||
|
}
|
||||||
|
bits -= engine.WOTS_LOGW;
|
||||||
|
output[outOff++] = ((total >>> bits) & (w - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] pkFromSig(byte[] sig, byte[] M, byte[] pkSeed, ADRS adrs)
|
||||||
|
{
|
||||||
|
ADRS wotspkADRS = new ADRS(adrs);
|
||||||
|
|
||||||
|
int[] msg = new int[engine.WOTS_LEN];
|
||||||
|
|
||||||
|
// convert message to base w
|
||||||
|
base_w(M, 0, w, msg, 0, engine.WOTS_LEN1);
|
||||||
|
|
||||||
|
// compute checksum
|
||||||
|
int csum = 0;
|
||||||
|
for (int i = 0; i < engine.WOTS_LEN1; i++ )
|
||||||
|
{
|
||||||
|
csum += w - 1 - msg[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert csum to base w
|
||||||
|
csum = csum << (8 - ((engine.WOTS_LEN2 * engine.WOTS_LOGW) % 8));
|
||||||
|
int len_2_bytes = (engine.WOTS_LEN2 * engine.WOTS_LOGW + 7) / 8;
|
||||||
|
byte[] csum_bytes = Pack.intToBigEndian(csum);
|
||||||
|
base_w(csum_bytes, 4 - len_2_bytes, w, msg, engine.WOTS_LEN1, engine.WOTS_LEN2);
|
||||||
|
|
||||||
|
byte[] sigI = new byte[engine.N];
|
||||||
|
byte[][] tmp = new byte[engine.WOTS_LEN][];
|
||||||
|
for (int i = 0; i < engine.WOTS_LEN; i++ )
|
||||||
|
{
|
||||||
|
adrs.setChainAddress(i);
|
||||||
|
System.arraycopy(sig, i * engine.N, sigI, 0, engine.N);
|
||||||
|
tmp[i] = chain(sigI, msg[i], w - 1 - msg[i], pkSeed, adrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
wotspkADRS.setTypeAndClear(ADRS.WOTS_PK);
|
||||||
|
wotspkADRS.setKeyPairAddress(adrs.getKeyPairAddress());
|
||||||
|
|
||||||
|
return engine.T_l(pkSeed, wotspkADRS, Arrays.concatenate(tmp));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to Aere Network.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
|
||||||
|
* compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||||
|
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
* implied. See the License for the specific language governing permissions and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-337 (2026-09-04): the extra wait before a proposal at an ANCHOR height, and only there.
|
||||||
|
*
|
||||||
|
* <p>The measured defect: with the hybrid certificate live on chain 2800 the proposal timer fired
|
||||||
|
* before the sixth SLH-DSA seal of the parent arrived, the producer refused, and eleven of twelve
|
||||||
|
* anchors paid the four-second round-change timeout. The fix is local timing, so the pinned
|
||||||
|
* properties are exactly two: the value is refused when it is nonsense (a bad delay must stop the
|
||||||
|
* node at start, not surprise the state machine), and it is added at anchor heights ONLY, so an
|
||||||
|
* ordinary block is never slowed by a millisecond.
|
||||||
|
*/
|
||||||
|
class AereAnchorProposalDelayTest {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
@AfterEach
|
||||||
|
void clean() {
|
||||||
|
System.clearProperty(AereAnchorProposalDelay.PROPERTY);
|
||||||
|
AereAnchorProposalDelay.forgetForTesting();
|
||||||
|
PqAnchorProducer.useConfigForTesting(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unsetMeansExactlyTodaysBehaviour() {
|
||||||
|
assertThat(AereAnchorProposalDelay.configuredMillis()).isZero();
|
||||||
|
assertThat(AereAnchorProposalDelay.millisFor(13_014_016L)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonsenseIsRefused() {
|
||||||
|
assertThatThrownBy(() -> AereAnchorProposalDelay.parse("later"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("AERE-PQC-ANCHOR-CONF-05");
|
||||||
|
assertThatThrownBy(() -> AereAnchorProposalDelay.parse("-1"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("outside");
|
||||||
|
assertThatThrownBy(() -> AereAnchorProposalDelay.parse("5001"))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("outside");
|
||||||
|
assertThat(AereAnchorProposalDelay.parse("1200")).isEqualTo(1200L);
|
||||||
|
assertThat(AereAnchorProposalDelay.parse(null)).isZero();
|
||||||
|
assertThat(AereAnchorProposalDelay.parse(" ")).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theWaitAppliesAtAnchorHeightsAndNowhereElse() {
|
||||||
|
// the live shape of chain 2800: anchors from 13,014,000, every 32nd block
|
||||||
|
final PqAnchorConfig cfg =
|
||||||
|
new PqAnchorConfig(
|
||||||
|
2800L,
|
||||||
|
13_014_000L,
|
||||||
|
Map.of(13_014_000L, 6),
|
||||||
|
OptionalInt.empty(),
|
||||||
|
false,
|
||||||
|
OptionalInt.empty(),
|
||||||
|
OptionalInt.of(32),
|
||||||
|
13_014_000L);
|
||||||
|
PqAnchorProducer.useConfigForTesting(cfg);
|
||||||
|
System.setProperty(AereAnchorProposalDelay.PROPERTY, "1200");
|
||||||
|
AereAnchorProposalDelay.forgetForTesting();
|
||||||
|
|
||||||
|
assertThat(AereAnchorProposalDelay.configuredMillis()).isEqualTo(1200L);
|
||||||
|
assertThat(cfg.anchorAppliesAt(13_014_032L)).isTrue();
|
||||||
|
assertThat(AereAnchorProposalDelay.millisFor(13_014_032L)).isEqualTo(1200L);
|
||||||
|
|
||||||
|
// the parent of an anchor, and an ordinary block: not a millisecond
|
||||||
|
assertThat(AereAnchorProposalDelay.millisFor(13_014_031L)).isZero();
|
||||||
|
assertThat(AereAnchorProposalDelay.millisFor(13_014_033L)).isZero();
|
||||||
|
// below the activation height nothing is an anchor
|
||||||
|
assertThat(AereAnchorProposalDelay.millisFor(13_013_968L)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aChainWithoutAnAnchorNeverWaits() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(PqAnchorConfig.never(2800L));
|
||||||
|
System.setProperty(AereAnchorProposalDelay.PROPERTY, "1200");
|
||||||
|
AereAnchorProposalDelay.forgetForTesting();
|
||||||
|
assertThat(AereAnchorProposalDelay.millisFor(13_014_032L)).isZero();
|
||||||
|
assertThat(AereAnchorProposalDelay.millisFor(1L)).isZero();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -244,7 +244,9 @@ public class PqAnchorConfigTest {
|
|||||||
// set, with no anchor height, is no longer "a node with no anchor configuration"; it becomes
|
// set, with no anchor height, is no longer "a node with no anchor configuration"; it becomes
|
||||||
// one with an incomplete configuration, and it refuses to start. A cap or an interval with no
|
// one with an incomplete configuration, and it refuses to start. A cap or an interval with no
|
||||||
// anchor means nothing, and it is better for it to shout than to keep quiet.
|
// anchor means nothing, and it is better for it to shout than to keep quiet.
|
||||||
assertThat(reader.accesses).hasSize(14);
|
// 16 since 2026-09-03: the v2 activation height (property + environment) joined the names
|
||||||
|
// 18 since 2026-09-04: the interval SCHEDULE (D-336, disk) joined them, between the interval and the v2 height
|
||||||
|
assertThat(reader.accesses).hasSize(18);
|
||||||
assertThat(reader.accesses)
|
assertThat(reader.accesses)
|
||||||
.containsExactly(
|
.containsExactly(
|
||||||
"property:" + PqAnchorConfig.PROPERTY_ANCHOR_BLOCK,
|
"property:" + PqAnchorConfig.PROPERTY_ANCHOR_BLOCK,
|
||||||
@ -261,6 +263,12 @@ public class PqAnchorConfigTest {
|
|||||||
"environment:" + PqAnchorConfig.ENV_MAX_SEALS,
|
"environment:" + PqAnchorConfig.ENV_MAX_SEALS,
|
||||||
"property:" + PqAnchorConfig.PROPERTY_ANCHOR_INTERVAL,
|
"property:" + PqAnchorConfig.PROPERTY_ANCHOR_INTERVAL,
|
||||||
"environment:" + PqAnchorConfig.ENV_ANCHOR_INTERVAL,
|
"environment:" + PqAnchorConfig.ENV_ANCHOR_INTERVAL,
|
||||||
|
"property:" + PqAnchorConfig.PROPERTY_ANCHOR_INTERVAL_SCHEDULE,
|
||||||
|
"environment:" + PqAnchorConfig.ENV_ANCHOR_INTERVAL_SCHEDULE,
|
||||||
|
// added 2026-09-03 with the scheme-tagged (v2) certificate: its activation height sits
|
||||||
|
// between the interval and the disarm, as in ANCHOR_NAMES
|
||||||
|
"property:" + PqAnchorConfig.PROPERTY_ANCHOR_V2_BLOCK,
|
||||||
|
"environment:" + PqAnchorConfig.ENV_ANCHOR_V2_BLOCK,
|
||||||
"property:" + PqAnchorConfig.PROPERTY_DISABLE,
|
"property:" + PqAnchorConfig.PROPERTY_DISABLE,
|
||||||
"environment:" + PqAnchorConfig.ENV_DISABLE);
|
"environment:" + PqAnchorConfig.ENV_DISABLE);
|
||||||
}
|
}
|
||||||
@ -694,4 +702,53 @@ public class PqAnchorConfigTest {
|
|||||||
assertThatThrownBy(PqAnchorConfig::fromSystemConfiguration)
|
assertThatThrownBy(PqAnchorConfig::fromSystemConfiguration)
|
||||||
.hasMessageContaining("SOURCE environment variable " + PqAnchorConfig.ENV_ANCHOR_BLOCK);
|
.hasMessageContaining("SOURCE environment variable " + PqAnchorConfig.ENV_ANCHOR_BLOCK);
|
||||||
}
|
}
|
||||||
|
// ---- D-336: the interval schedule -------------------------------------------------------------
|
||||||
|
|
||||||
|
private static PqAnchorConfig every32() {
|
||||||
|
return new PqAnchorConfig(2800L, 1000L, schedule(), OptionalInt.empty(), false)
|
||||||
|
.withAnchorInterval(OptionalInt.of(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aScheduledIntervalKeepsTheOldGridBelowTheHeightAndThinsItAbove() {
|
||||||
|
// 1384 = 1000 + 3 * 128: on the new grid AND an old-grid anchor (384 % 32 == 0)
|
||||||
|
final PqAnchorConfig c = every32().withAnchorIntervalSchedule(java.util.Map.of(1384L, 128));
|
||||||
|
assertThat(c.intervalAt(1383L)).isEqualTo(32);
|
||||||
|
assertThat(c.intervalAt(1384L)).isEqualTo(128);
|
||||||
|
assertThat(c.isAnchorHeight(1032L)).isTrue();
|
||||||
|
assertThat(c.isAnchorHeight(1048L)).isFalse();
|
||||||
|
assertThat(c.isAnchorHeight(1352L)).isTrue(); // last old-grid anchor before the change
|
||||||
|
assertThat(c.isAnchorHeight(1384L)).isTrue(); // the activation height is itself an anchor
|
||||||
|
assertThat(c.isAnchorHeight(1416L)).isFalse(); // 1384 + 32: no longer an anchor
|
||||||
|
assertThat(c.isAnchorHeight(1512L)).isTrue(); // 1384 + 128
|
||||||
|
// every anchor of the new regime was an anchor of the old one
|
||||||
|
for (long h = 1384L; h < 1384L + 128L * 20; h++) {
|
||||||
|
if (c.isAnchorHeight(h)) {
|
||||||
|
assertThat(every32().isAnchorHeight(h)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aScheduleHeightOffTheNewGridIsRefused() {
|
||||||
|
assertThatThrownBy(() -> every32().withAnchorIntervalSchedule(java.util.Map.of(1400L, 128)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("not on the new grid");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anIntervalThatIsNotAMultipleOfTheBaseIsRefused() {
|
||||||
|
assertThatThrownBy(() -> every32().withAnchorIntervalSchedule(java.util.Map.of(1400L, 100)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("not a positive multiple");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aScheduleWithoutABaseIntervalIsRefused() {
|
||||||
|
final PqAnchorConfig everyBlock = new PqAnchorConfig(2800L, 1000L, schedule(), OptionalInt.empty(), false);
|
||||||
|
assertThatThrownBy(() -> everyBlock.withAnchorIntervalSchedule(java.util.Map.of(1384L, 128)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessageContaining("cannot create one");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,326 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to Aere Network.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
|
||||||
|
* compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||||
|
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
* implied. See the License for the specific language governing permissions and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.common.bft;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.withSettings;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||||
|
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
|
||||||
|
import org.hyperledger.besu.crypto.SecureRandomProvider;
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
import org.hyperledger.besu.ethereum.ProtocolContext;
|
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.bouncycastle.crypto.digests.KeccakDigest;
|
||||||
|
import org.bouncycastle.pqc.crypto.falcon.FalconPrivateKeyParameters;
|
||||||
|
import org.bouncycastle.pqc.crypto.falcon.FalconSigner;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE ANCHOR V2 (2026-09-03): the PROPOSER writes the SCHEME-TAGGED certificate from the v2 height.
|
||||||
|
*
|
||||||
|
* <p>Same harness as {@link PqAnchorProducerCostTest} (seven Falcon probe keys anchored in a genesis
|
||||||
|
* registry, the seal cache loaded by hand, a mocked validator provider), plus a hybrid registry with
|
||||||
|
* seven SLH-DSA-SHA2-128s probe keys and the scheme schedule, loaded through the REAL system
|
||||||
|
* configuration path so the producer finds them exactly where a node would.
|
||||||
|
*
|
||||||
|
* <p>Proven, each with its pair: at a v2 height the certificate is [Falcon x K + SLH-DSA x K] under the
|
||||||
|
* v2 digest and the v1 list is EMPTY; below the v2 height nothing changes (v1 list, v1 digest); with
|
||||||
|
* no SLH-DSA seal heard the proposer REFUSES rather than writing a certificate short of a scheme; an
|
||||||
|
* SLH-DSA seal from an index without a Falcon seal is not counted; a Falcon-only schedule yields a v2
|
||||||
|
* certificate that is Falcon-only.
|
||||||
|
*/
|
||||||
|
class PqAnchorV2ProducerTest {
|
||||||
|
|
||||||
|
private static final long CHAIN_ID = 220_879L;
|
||||||
|
private static final long H = 1_000L;
|
||||||
|
private static final long V2 = 1_005L;
|
||||||
|
private static final int N = 7;
|
||||||
|
private static final int K = 3;
|
||||||
|
private static final String SLH = "slh-dsa-sha2-128s";
|
||||||
|
|
||||||
|
@TempDir private Path tmp;
|
||||||
|
|
||||||
|
private final List<FalconPrivateKeyParameters> falconKeys = new ArrayList<>();
|
||||||
|
private final List<SealScheme.GeneratedPair> slhKeys = new ArrayList<>();
|
||||||
|
private final List<Address> validators = new ArrayList<>();
|
||||||
|
private final SecureRandom random = SecureRandomProvider.createSecureRandom();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
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++) {
|
||||||
|
falconKeys.add(PqV2Fixture.privateKey(i));
|
||||||
|
validators.add(PqV2Fixture.address(i));
|
||||||
|
final byte[] row = PqV2Fixture.anchorPreimageRow(i);
|
||||||
|
kd.update(row, 0, row.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("\"}}}}");
|
||||||
|
final Path genesis = tmp.resolve("genesis-registry.json");
|
||||||
|
Files.writeString(genesis, manifest.toString());
|
||||||
|
System.setProperty("aere.falcon.genesis", genesis.toAbsolutePath().toString());
|
||||||
|
resetFalconSingleton();
|
||||||
|
PqSealCache.instance().clear();
|
||||||
|
armHybrid("0:falcon-512+" + SLH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() throws Exception {
|
||||||
|
System.clearProperty("aere.falcon.genesis");
|
||||||
|
System.clearProperty(HybridSealSupport.PROPERTY_SCHEDULE);
|
||||||
|
System.clearProperty(HybridSealSupport.PROPERTY_REGISTRY);
|
||||||
|
HybridSealSupport.resetForTesting();
|
||||||
|
resetFalconSingleton();
|
||||||
|
PqSealCache.instance().clear();
|
||||||
|
PqAnchorProducer.useConfigForTesting(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes a hybrid-1 registry with the seven Falcon addresses and seven SLH-DSA keys, and loads it. */
|
||||||
|
private void armHybrid(final String schedule) throws Exception {
|
||||||
|
if (slhKeys.isEmpty()) {
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
slhKeys.add(SealSchemes.SLH_DSA_128S.generate(random));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final StringBuilder p = new StringBuilder();
|
||||||
|
p.append("formatVersion=hybrid-1\nchainId=").append(CHAIN_ID).append("\ncount=").append(N).append('\n');
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
p.append(i).append(".addr=").append(validators.get(i).toHexString()).append('\n');
|
||||||
|
p.append(i).append(".key.").append(SLH).append('=')
|
||||||
|
.append(Bytes.wrap(slhKeys.get(i).publicRegistryForm()).toHexString()).append('\n');
|
||||||
|
}
|
||||||
|
final Path reg = tmp.resolve("hybrid-registry-" + schedule.hashCode() + ".properties");
|
||||||
|
Files.writeString(reg, p.toString());
|
||||||
|
System.setProperty(HybridSealSupport.PROPERTY_SCHEDULE, schedule);
|
||||||
|
System.setProperty(HybridSealSupport.PROPERTY_REGISTRY, reg.toAbsolutePath().toString());
|
||||||
|
HybridSealSupport.resetForTesting();
|
||||||
|
assertThat(HybridSealSupport.instance().registry()).describedAs("hybrid registry loaded").isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------ proofs
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void atTheV2HeightTheProposerWritesASchemeTaggedCertificateUnderTheV2Digest() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(config(V2, OptionalInt.of(K)));
|
||||||
|
final BlockHeader parent = parentOf(V2 + 9L);
|
||||||
|
hearFalcon(parent, N);
|
||||||
|
hearExtras(parent, N);
|
||||||
|
final BftExtraData produced = PqAnchorProducer.apply(base(), parent, contextWith(validators));
|
||||||
|
assertThat(produced.getFalconSeals()).describedAs("the v1 list is EMPTY on a v2 header").isEmpty();
|
||||||
|
final List<SchemeSeal> tagged = produced.getHybridSeals();
|
||||||
|
assertThat(PqAnchorV2.distinctValidatorsWith(tagged, SealSchemes.FALCON_512.wireId()))
|
||||||
|
.describedAs("K Falcon seals, capped exactly as the v1 producer caps them")
|
||||||
|
.isEqualTo(K);
|
||||||
|
assertThat(PqAnchorV2.distinctValidatorsWith(tagged, SealSchemes.SLH_DSA_128S.wireId()))
|
||||||
|
.describedAs("K SLH-DSA seals, each bound to a Falcon-certified index")
|
||||||
|
.isEqualTo(K);
|
||||||
|
for (final SchemeSeal s : tagged) {
|
||||||
|
if (s.getSchemeWireId() == SealSchemes.SLH_DSA_128S.wireId()) {
|
||||||
|
assertThat(tagged.stream().anyMatch(f -> f.getSchemeWireId() == SealSchemes.FALCON_512.wireId()
|
||||||
|
&& f.getValidatorIndex() == s.getValidatorIndex()))
|
||||||
|
.describedAs("SLH-DSA seal of index %s rides with a Falcon seal of the same index", s.getValidatorIndex())
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(produced.getVanityData())
|
||||||
|
.describedAs("vanityData is the v2 digest over exactly the carried certificate")
|
||||||
|
.isEqualTo(PqAnchorV2.anchorDigestV2(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(), tagged));
|
||||||
|
// and NOT the v1 digest over the Falcon half, which is what a v1 rule would recompute
|
||||||
|
final List<FalconSeal> falconHalf = new ArrayList<>();
|
||||||
|
for (final SchemeSeal s : tagged) {
|
||||||
|
if (s.getSchemeWireId() == SealSchemes.FALCON_512.wireId()) {
|
||||||
|
falconHalf.add(new FalconSeal(s.getValidatorIndex(), s.getSignature()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(produced.getVanityData())
|
||||||
|
.isNotEqualTo(PqAnchor.anchorDigest(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(), falconHalf));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void belowTheV2HeightNothingChangesEvenWithExtrasHeard() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(config(V2, OptionalInt.of(K)));
|
||||||
|
final BlockHeader parent = parentOf(V2 - 2L); // block V2-1 is an anchor height below V2
|
||||||
|
hearFalcon(parent, N);
|
||||||
|
hearExtras(parent, N);
|
||||||
|
final BftExtraData produced = PqAnchorProducer.apply(base(), parent, contextWith(validators));
|
||||||
|
assertThat(produced.getHybridSeals()).describedAs("no scheme-tagged certificate below V2").isEmpty();
|
||||||
|
assertThat(produced.getFalconSeals()).hasSize(K);
|
||||||
|
assertThat(produced.getVanityData())
|
||||||
|
.isEqualTo(PqAnchor.anchorDigest(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes(),
|
||||||
|
PqAnchor.sortedByIndex(new ArrayList<>(produced.getFalconSeals()))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void withNoSlhDsaSealHeardTheProposerRefusesInsteadOfWritingAShortCertificate() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(config(V2, OptionalInt.of(K)));
|
||||||
|
final BlockHeader parent = parentOf(V2 + 9L);
|
||||||
|
hearFalcon(parent, N);
|
||||||
|
// nothing recorded in the extras slot
|
||||||
|
assertThatThrownBy(() -> PqAnchorProducer.apply(base(), parent, contextWith(validators)))
|
||||||
|
.isInstanceOf(PqAnchorNotReadyException.class)
|
||||||
|
.hasMessageContaining(SLH);
|
||||||
|
// and with only K-1 SLH-DSA seals it still refuses: K per scheme, not "some"
|
||||||
|
hearExtras(parent, K - 1);
|
||||||
|
assertThatThrownBy(() -> PqAnchorProducer.apply(base(), parent, contextWith(validators)))
|
||||||
|
.isInstanceOf(PqAnchorNotReadyException.class)
|
||||||
|
.hasMessageContaining(SLH);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anSlhDsaSealFromAnIndexWithoutAFalconSealIsNotCounted() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(config(V2, OptionalInt.of(K)));
|
||||||
|
final BlockHeader parent = parentOf(V2 + 9L);
|
||||||
|
hearFalcon(parent, K); // Falcon from indices 0..K-1 only
|
||||||
|
// SLH-DSA from indices K..N-1 only: none of them is Falcon-certified
|
||||||
|
final Bytes32 m = PqAnchor.commitMessage(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes());
|
||||||
|
final List<SchemeSeal> extras = new ArrayList<>();
|
||||||
|
for (int i = K; i < N; i++) {
|
||||||
|
extras.add(new SchemeSeal(SealSchemes.SLH_DSA_128S.wireId(), i, Bytes.wrap(slhSign(i, m))));
|
||||||
|
}
|
||||||
|
PqSealCache.instance().recordExtras(parent.getNumber(), parent.getHash(), extras);
|
||||||
|
assertThatThrownBy(() -> PqAnchorProducer.apply(base(), parent, contextWith(validators)))
|
||||||
|
.isInstanceOf(PqAnchorNotReadyException.class)
|
||||||
|
.hasMessageContaining("bound to a Falcon-certified index");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aFalconOnlyScheduleYieldsAFalconOnlyV2Certificate() throws Exception {
|
||||||
|
armHybrid("0:falcon-512");
|
||||||
|
PqAnchorProducer.useConfigForTesting(config(V2, OptionalInt.of(K)));
|
||||||
|
final BlockHeader parent = parentOf(V2 + 9L);
|
||||||
|
hearFalcon(parent, N);
|
||||||
|
hearExtras(parent, N); // heard, but the schedule does not ask for them
|
||||||
|
final BftExtraData produced = PqAnchorProducer.apply(base(), parent, contextWith(validators));
|
||||||
|
assertThat(produced.getHybridSeals()).hasSize(K);
|
||||||
|
assertThat(PqAnchorV2.distinctValidatorsWith(produced.getHybridSeals(), SealSchemes.SLH_DSA_128S.wireId()))
|
||||||
|
.describedAs("a scheme the schedule does not name is never written, heard or not")
|
||||||
|
.isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theSealCacheKeepsExtrasBesideTheFalconSealsUnderTheSameKey() {
|
||||||
|
final BlockHeader parent = parentOf(V2 + 9L);
|
||||||
|
hearFalcon(parent, N);
|
||||||
|
hearExtras(parent, N);
|
||||||
|
assertThat(PqSealCache.instance().sealsFor(parent.getNumber(), parent.getHash())).hasSize(N);
|
||||||
|
final List<SchemeSeal> extras = PqSealCache.instance().extrasFor(parent.getNumber(), parent.getHash());
|
||||||
|
assertThat(extras).hasSize(N);
|
||||||
|
assertThat(extras).isSortedAccordingTo(PqAnchorV2.CANONICAL);
|
||||||
|
// a Falcon-tagged seal offered to the extras slot is dropped: Falcon has its own slot
|
||||||
|
PqSealCache.instance().recordExtras(parent.getNumber(), parent.getHash(),
|
||||||
|
List.of(new SchemeSeal(SealSchemes.FALCON_512.wireId(), 0, Bytes.of(1, 2, 3))));
|
||||||
|
assertThat(PqSealCache.instance().extrasFor(parent.getNumber(), parent.getHash())).hasSize(N);
|
||||||
|
// and a different hash under the same height is a different entry
|
||||||
|
assertThat(PqSealCache.instance().extrasFor(parent.getNumber(), parentOf(V2 + 8L).getHash())).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------ helpers
|
||||||
|
|
||||||
|
private static PqAnchorConfig config(final long v2Block, final OptionalInt cap) {
|
||||||
|
return new PqAnchorConfig(
|
||||||
|
CHAIN_ID, H, Map.of(H, K), OptionalInt.empty(), false, cap, OptionalInt.empty(), v2Block);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockHeader parentOf(final long number) {
|
||||||
|
return new BlockHeaderTestFixture().number(number).buildHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BftExtraData base() {
|
||||||
|
return new BftExtraData(
|
||||||
|
Bytes32.ZERO, Collections.emptyList(), Optional.empty(), 0, validators, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hearFalcon(final BlockHeader parent, final int howMany) {
|
||||||
|
final Bytes32 m = PqAnchor.commitMessage(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes());
|
||||||
|
final List<FalconSeal> heard = new ArrayList<>();
|
||||||
|
for (int i = 0; i < howMany; i++) {
|
||||||
|
heard.add(new FalconSeal(i, Bytes.wrap(falconSign(falconKeys.get(i), m))));
|
||||||
|
}
|
||||||
|
PqSealCache.instance().record(parent.getNumber(), parent.getHash(), heard);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hearExtras(final BlockHeader parent, final int howMany) {
|
||||||
|
final Bytes32 m = PqAnchor.commitMessage(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes());
|
||||||
|
final List<SchemeSeal> extras = new ArrayList<>();
|
||||||
|
for (int i = 0; i < howMany; i++) {
|
||||||
|
extras.add(new SchemeSeal(SealSchemes.SLH_DSA_128S.wireId(), i, Bytes.wrap(slhSign(i, m))));
|
||||||
|
}
|
||||||
|
PqSealCache.instance().recordExtras(parent.getNumber(), parent.getHash(), extras);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] slhSign(final int index, final Bytes32 m) {
|
||||||
|
return SealSchemes.SLH_DSA_128S.sign(slhKeys.get(index).privateKey(), m.toArray()).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] falconSign(final FalconPrivateKeyParameters key, final Bytes32 m) {
|
||||||
|
final FalconSigner signer = new FalconSigner();
|
||||||
|
signer.init(true, key);
|
||||||
|
return signer.generateSignature(m.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtocolContext contextWith(final Collection<Address> vs) {
|
||||||
|
final ValidatorProvider validatorProvider =
|
||||||
|
mock(ValidatorProvider.class, withSettings().strictness(Strictness.LENIENT));
|
||||||
|
when(validatorProvider.getValidatorsForBlock(any())).thenReturn(vs);
|
||||||
|
when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(vs);
|
||||||
|
final BftContext bftContext =
|
||||||
|
mock(BftContext.class, withSettings().strictness(Strictness.LENIENT));
|
||||||
|
when(bftContext.getValidatorProvider()).thenReturn(validatorProvider);
|
||||||
|
when(bftContext.as(any())).thenReturn(bftContext);
|
||||||
|
return new ProtocolContext.Builder().withConsensusContext(bftContext).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void resetFalconSingleton() throws Exception {
|
||||||
|
final Field f = FalconSealSupport.class.getDeclaredField("instance");
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -620,4 +620,33 @@ public class PqSealPersistenceTest {
|
|||||||
f.setAccessible(true);
|
f.setAccessible(true);
|
||||||
f.set(null, null);
|
f.set(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-330 (2026-09-03, testnet 28001): the main file followed the highest height HEARD, which was a
|
||||||
|
* proposal that never imported (168032), so after a fleet-wide restart no node held the seals of
|
||||||
|
* the head (168031). The previous slot keeps them one height longer.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void theSealsOfTheHeadSurviveTheStoreMovingToALaterHeightThatNeverImported() {
|
||||||
|
PqSealCache.instance().enablePersistence(dataDirectory, CHAIN_ID);
|
||||||
|
for (int i = 0; i < K; i++) {
|
||||||
|
PqSealCache.instance().record(head.getNumber(), head.getHash(), List.of(genuineSeal(i)));
|
||||||
|
}
|
||||||
|
// the fleet now hears commits for a NEXT block that will never import
|
||||||
|
final BlockHeader next = new BlockHeaderTestFixture().number(head.getNumber() + 1L).buildHeader();
|
||||||
|
final Bytes32 mNext = PqAnchor.commitMessage(CHAIN_ID, next.getNumber(), next.getHash().getBytes());
|
||||||
|
final FalconSigner signer = new FalconSigner();
|
||||||
|
signer.init(true, privateKeys.get(0));
|
||||||
|
PqSealCache.instance()
|
||||||
|
.record(next.getNumber(), next.getHash(),
|
||||||
|
List.of(new FalconSeal(0, Bytes.wrap(signer.generateSignature(mNext.toArray())))));
|
||||||
|
assertThat(PqSealStore.previousFileIn(dataDirectory)).describedAs("the previous slot exists after the rotation").exists();
|
||||||
|
// restart: memory gone, the main file holds the never-imported height
|
||||||
|
PqSealCache.instance().clear();
|
||||||
|
final int restored =
|
||||||
|
PqSealCache.instance()
|
||||||
|
.restoreFromDisk(head.getNumber(), head.getHash(), PqSignerRegistry.falconSealSupport());
|
||||||
|
assertThat(restored).describedAs("the head's seals come back from the previous slot").isEqualTo(K);
|
||||||
|
assertThat(PqAnchorProducer.apply(base, head, context).getFalconSeals()).hasSize(K);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to 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 java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-325 (2026-09-03), the CROSS-IMPLEMENTATION vector: an SLH-DSA-SHA2-128s signature produced by the public
|
||||||
|
* verifier's library (@noble/post-quantum 0.7.1, FIPS 205 external form, empty context) must verify under this
|
||||||
|
* fork's {@link SlhDsaSealScheme} (BouncyCastle Java). Two implementations agreeing on one vector is the only
|
||||||
|
* proof that "the same form" is the same form; two self-consistent implementations prove nothing about each other.
|
||||||
|
*
|
||||||
|
* <p>With its pair: the same signature over a message that differs in one bit does NOT verify, and a signature with
|
||||||
|
* one bit flipped does NOT verify. A verifier that accepted everything would pass the positive half alone.
|
||||||
|
*/
|
||||||
|
class SlhDsaCrossVectorTest {
|
||||||
|
|
||||||
|
private static String field(final String json, final String name) {
|
||||||
|
final Matcher m = Pattern.compile("\"" + name + "\"\\s*:\\s*\"([0-9a-fA-F]+)\"").matcher(json);
|
||||||
|
if (!m.find()) {
|
||||||
|
throw new IllegalStateException("vector has no field " + name);
|
||||||
|
}
|
||||||
|
return m.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String vector() throws IOException {
|
||||||
|
try (InputStream in =
|
||||||
|
SlhDsaCrossVectorTest.class.getResourceAsStream("/vector-slh-dsa-noble-2026-09-03.json")) {
|
||||||
|
if (in == null) {
|
||||||
|
throw new IllegalStateException("vector-slh-dsa-noble-2026-09-03.json is not on the test classpath");
|
||||||
|
}
|
||||||
|
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aSignatureFromTheJavaScriptImplementationVerifiesHereAndItsTamperedTwinsDoNot() throws IOException {
|
||||||
|
final String v = vector();
|
||||||
|
final byte[] pk = Bytes.fromHexString(field(v, "pk")).toArray();
|
||||||
|
final byte[] msg = Bytes.fromHexString(field(v, "msg")).toArray();
|
||||||
|
final byte[] sig = Bytes.fromHexString(field(v, "sig")).toArray();
|
||||||
|
assertThat(pk).hasSize(32);
|
||||||
|
assertThat(msg).hasSize(32);
|
||||||
|
assertThat(sig).hasSize(7856);
|
||||||
|
final SealScheme scheme = SealSchemes.SLH_DSA_128S;
|
||||||
|
assertThat(scheme.verifyRaw(pk, msg, sig))
|
||||||
|
.describedAs("the @noble/post-quantum signature verifies under BouncyCastle's SLH-DSA-SHA2-128s (external form)")
|
||||||
|
.isTrue();
|
||||||
|
final byte[] otherMsg = msg.clone();
|
||||||
|
otherMsg[0] ^= 0x01;
|
||||||
|
assertThat(scheme.verifyRaw(pk, otherMsg, sig)).describedAs("one message bit flipped").isFalse();
|
||||||
|
final byte[] badSig = sig.clone();
|
||||||
|
badSig[badSig.length / 2] ^= 0x01;
|
||||||
|
assertThat(scheme.verifyRaw(pk, msg, badSig)).describedAs("one signature bit flipped").isFalse();
|
||||||
|
// and the INTERNAL form (what the precompile at 0x0AE4 uses) must NOT be confused with it: a scheme that
|
||||||
|
// accepted both would let a seal stand in for a precompile call or the other way round.
|
||||||
|
assertThat(SealSchemes.byId("slh-dsa-sha2-128s")).isPresent();
|
||||||
|
assertThat(SealSchemes.byId("slh-dsa-128s")).describedAs("the pre-D-325 alias still resolves").isPresent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to 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 java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAKeyGenerationParameters;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAKeyPairGenerator;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAParameters;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAPrivateKeyParameters;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSAPublicKeyParameters;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.slhdsa.SLHDSASigner;
|
||||||
|
import org.hyperledger.besu.crypto.SecureRandomProvider;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-337 (2026-09-04): the in-tree SLH-DSA engine (Bouncy Castle's algorithm on the JDK's SHA-NI
|
||||||
|
* digest) must be the SAME signature scheme as the Bouncy Castle original, bit for bit. Otherwise a
|
||||||
|
* faster signer is a different certificate, and the second client and the public verifier would
|
||||||
|
* reject every anchor. Each direction is pinned: fast-signed verifies under the original, original-
|
||||||
|
* signed verifies under the fast engine, deterministic signing gives identical bytes, and the fast
|
||||||
|
* engine still rejects a tampered signature (the negative control of the verifier).
|
||||||
|
*/
|
||||||
|
class SlhDsaFastEngineTest {
|
||||||
|
|
||||||
|
private static final org.bouncycastle.pqc.crypto.slhdsa.SLHDSAParameters ORIGINAL =
|
||||||
|
org.bouncycastle.pqc.crypto.slhdsa.SLHDSAParameters.sha2_128s;
|
||||||
|
|
||||||
|
private static AsymmetricCipherKeyPair fastKeyPair() {
|
||||||
|
final SLHDSAKeyPairGenerator gen = new SLHDSAKeyPairGenerator();
|
||||||
|
gen.init(new SLHDSAKeyGenerationParameters(SecureRandomProvider.createSecureRandom(), SLHDSAParameters.sha2_128s));
|
||||||
|
return gen.generateKeyPair();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPublicKeyParameters originalPub(
|
||||||
|
final AsymmetricCipherKeyPair kp) {
|
||||||
|
return new org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPublicKeyParameters(
|
||||||
|
ORIGINAL, ((SLHDSAPublicKeyParameters) kp.getPublic()).getEncoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPrivateKeyParameters originalPriv(
|
||||||
|
final AsymmetricCipherKeyPair kp) {
|
||||||
|
return new org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPrivateKeyParameters(
|
||||||
|
ORIGINAL, ((SLHDSAPrivateKeyParameters) kp.getPrivate()).getEncoded());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aFastSignatureVerifiesUnderTheOriginalBouncyCastleEngine() {
|
||||||
|
final AsymmetricCipherKeyPair kp = fastKeyPair();
|
||||||
|
final byte[] msg = "AERE-PQ-ANCHOR-2 commit seal message".getBytes(StandardCharsets.UTF_8);
|
||||||
|
final SLHDSASigner signer = new SLHDSASigner();
|
||||||
|
signer.init(true, kp.getPrivate());
|
||||||
|
final byte[] sig = signer.generateSignature(msg);
|
||||||
|
assertThat(sig).hasSize(7856);
|
||||||
|
|
||||||
|
final org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner original =
|
||||||
|
new org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner();
|
||||||
|
original.init(false, originalPub(kp));
|
||||||
|
assertThat(original.verifySignature(msg, sig)).isTrue();
|
||||||
|
|
||||||
|
// negative control of the ORIGINAL verifier on the fast signature: one bit off, rejected
|
||||||
|
final byte[] bad = sig.clone();
|
||||||
|
bad[1234] ^= 0x01;
|
||||||
|
original.init(false, originalPub(kp));
|
||||||
|
assertThat(original.verifySignature(msg, bad)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anOriginalSignatureVerifiesUnderTheFastEngineAndATamperedOneDoesNot() {
|
||||||
|
final AsymmetricCipherKeyPair kp = fastKeyPair();
|
||||||
|
final byte[] msg = new byte[32];
|
||||||
|
msg[31] = 7;
|
||||||
|
final org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner original =
|
||||||
|
new org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner();
|
||||||
|
original.init(true, originalPriv(kp));
|
||||||
|
final byte[] sig = original.generateSignature(msg);
|
||||||
|
|
||||||
|
final SLHDSASigner fast = new SLHDSASigner();
|
||||||
|
fast.init(false, kp.getPublic());
|
||||||
|
assertThat(fast.verifySignature(msg, sig)).isTrue();
|
||||||
|
|
||||||
|
final byte[] bad = sig.clone();
|
||||||
|
bad[100] ^= 0x01;
|
||||||
|
fast.init(false, kp.getPublic());
|
||||||
|
assertThat(fast.verifySignature(msg, bad)).isFalse();
|
||||||
|
|
||||||
|
final byte[] otherMsg = msg.clone();
|
||||||
|
otherMsg[0] ^= 0x01;
|
||||||
|
fast.init(false, kp.getPublic());
|
||||||
|
assertThat(fast.verifySignature(otherMsg, sig)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deterministicSigningGivesTheSameBytesAsTheOriginal() {
|
||||||
|
// Without a SecureRandom both engines use the pk seed as opt_rand (FIPS 205 deterministic
|
||||||
|
// variant), so the SAME key and message must give the SAME 7856 bytes. This is the strongest
|
||||||
|
// statement that the hash swap changed nothing but the clock.
|
||||||
|
final AsymmetricCipherKeyPair kp = fastKeyPair();
|
||||||
|
final byte[] msg = "deterministic".getBytes(StandardCharsets.UTF_8);
|
||||||
|
final SLHDSASigner fast = new SLHDSASigner();
|
||||||
|
fast.init(true, kp.getPrivate());
|
||||||
|
final org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner original =
|
||||||
|
new org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner();
|
||||||
|
original.init(true, originalPriv(kp));
|
||||||
|
assertThat(Arrays.equals(fast.generateSignature(msg), original.generateSignature(msg)))
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void theSealSchemeItselfSignsThroughTheFastEngineAndTheOriginalAccepts() {
|
||||||
|
final SlhDsaSealScheme scheme = new SlhDsaSealScheme();
|
||||||
|
final SealScheme.GeneratedPair pair = scheme.generate(SecureRandomProvider.createSecureRandom());
|
||||||
|
final byte[] msg = new byte[32];
|
||||||
|
final byte[] sig = scheme.sign(pair.privateKey(), msg).orElseThrow();
|
||||||
|
final org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner original =
|
||||||
|
new org.bouncycastle.pqc.crypto.slhdsa.SLHDSASigner();
|
||||||
|
original.init(
|
||||||
|
false,
|
||||||
|
new org.bouncycastle.pqc.crypto.slhdsa.SLHDSAPublicKeyParameters(
|
||||||
|
ORIGINAL, pair.publicRegistryForm()));
|
||||||
|
assertThat(original.verifySignature(msg, sig)).isTrue();
|
||||||
|
assertThat(scheme.verifyRaw(pair.publicRegistryForm(), msg, sig)).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -68,6 +68,20 @@ public class Commit extends BftMessage<CommitPayload> {
|
|||||||
*
|
*
|
||||||
* @return the digest
|
* @return the digest
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* D-339 (2026-09-04): the hybrid extra seals this Commit carries, as a plain accessor.
|
||||||
|
*
|
||||||
|
* <p>It exists so the two places that keep seals out of a dying message (the late-seal salvage in
|
||||||
|
* the controller and the no-proposal path in the round) do not have to reach through the signed
|
||||||
|
* payload. Reaching through it also broke every test that mocks a Commit, which is exactly the
|
||||||
|
* kind of surprise a wrapper class is meant to absorb.
|
||||||
|
*
|
||||||
|
* @return the non-Falcon scheme seals, empty on every commit that carries none
|
||||||
|
*/
|
||||||
|
public java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> getExtraSeals() {
|
||||||
|
return getPayload().getExtraSeals();
|
||||||
|
}
|
||||||
|
|
||||||
public Hash getDigest() {
|
public Hash getDigest() {
|
||||||
return getPayload().getDigest();
|
return getPayload().getDigest();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -270,6 +270,14 @@ public class QbftController implements QbftEventHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PqSealCache.instance().record(head, headHeader.getHash(), List.of(seal.get()));
|
PqSealCache.instance().record(head, headHeader.getHash(), List.of(seal.get()));
|
||||||
|
// D-339 (2026-09-04): the hybrid extras ride the SAME late commits, and until today this path
|
||||||
|
// dropped them. The anchor needs K seals of EVERY scheme, so a salvaged Falcon seal without its
|
||||||
|
// SLH-DSA twin still left the proposer short and the anchor still lost its round.
|
||||||
|
final List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> extras =
|
||||||
|
commit.getExtraSeals();
|
||||||
|
if (!extras.isEmpty()) {
|
||||||
|
PqSealCache.instance().recordExtras(head, headHeader.getHash(), extras);
|
||||||
|
}
|
||||||
LOG.trace("AERE D-227: salvaged a late Falcon seal for imported block {}", head);
|
LOG.trace("AERE D-227: salvaged a late Falcon seal for imported block {}", head);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -512,6 +512,9 @@ public class QbftRound {
|
|||||||
// this node has already imported the block. Those late seals are exactly the ones that let a
|
// this node has already imported the block. Those late seals are exactly the ones that let a
|
||||||
// proposer reach the threshold K without waiting for its own round to be re-run.
|
// proposer reach the threshold K without waiting for its own round to be re-run.
|
||||||
roundState.getProposedBlock().ifPresent(this::pqCacheHeardSeals);
|
roundState.getProposedBlock().ifPresent(this::pqCacheHeardSeals);
|
||||||
|
if (roundState.getProposedBlock().isEmpty()) {
|
||||||
|
pqCacheSealsOfCommitWithoutProposal(msg);
|
||||||
|
}
|
||||||
// AERE hybrid PQC: attempt import on every commit while not yet imported. In the common case
|
// AERE hybrid PQC: attempt import on every commit while not yet imported. In the common case
|
||||||
// this fires exactly once (on the quorum-th commit). Post-fork, if the first attempt lacked a
|
// this fires exactly once (on the quorum-th commit). Post-fork, if the first attempt lacked a
|
||||||
// complete Falcon quorum certificate (e.g. one of the quorum's validators had a faulty Falcon
|
// complete Falcon quorum certificate (e.g. one of the quorum's validators had a faulty Falcon
|
||||||
@ -589,12 +592,52 @@ public class QbftRound {
|
|||||||
* on-chain hash, which is the key the producer of the next block looks under. Cheap and
|
* on-chain hash, which is the key the producer of the next block looks under. Cheap and
|
||||||
* idempotent: the cache keeps the first seal seen per validator index.
|
* idempotent: the cache keeps the first seal seen per validator index.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* D-339 (2026-09-04): keep the seals of a Commit even when this node has NO proposal for the
|
||||||
|
* round, keyed on the digest the Commit itself names.
|
||||||
|
*
|
||||||
|
* <p>Why, measured on chain 2800 the same evening: the proposer of an anchor kept refusing with
|
||||||
|
* "this node holds 1 valid eligible Falcon seal ... 0 rejected" - it was not rejecting seals, it
|
||||||
|
* had never heard them. The path above caches only when the round holds the proposed block, so a
|
||||||
|
* node that missed the proposal of the parent's round (or imported that block by any route other
|
||||||
|
* than its own round) collected the Commits and threw their seals away. It then could not build
|
||||||
|
* the certificate of the next anchor, the round expired, and every 32nd height paid the
|
||||||
|
* round-change timeout. Eleven of twelve anchors measured before this fix.
|
||||||
|
*
|
||||||
|
* <p>Safety: only the seal is copied, never the message; the digest is the one the Commit names,
|
||||||
|
* so a losing round writes an entry under a hash that never becomes canonical and is simply never
|
||||||
|
* looked up; and the producer Falcon-verifies every cached seal against the anchored registry
|
||||||
|
* before carrying it, so a seal that lies about its index still cannot reach a header.
|
||||||
|
*
|
||||||
|
* @param msg the commit whose seals are kept
|
||||||
|
*/
|
||||||
|
private void pqCacheSealsOfCommitWithoutProposal(final Commit msg) {
|
||||||
|
final long height = roundState.getRoundIdentifier().getSequenceNumber();
|
||||||
|
msg.getFalconSeal()
|
||||||
|
.ifPresent(
|
||||||
|
seal ->
|
||||||
|
PqSealCache.instance()
|
||||||
|
.record(height, msg.getDigest(), java.util.List.of(seal)));
|
||||||
|
final java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> extras =
|
||||||
|
msg.getExtraSeals();
|
||||||
|
if (!extras.isEmpty()) {
|
||||||
|
PqSealCache.instance().recordExtras(height, msg.getDigest(), extras);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void pqCacheHeardSeals(final QbftBlock block) {
|
private void pqCacheHeardSeals(final QbftBlock block) {
|
||||||
final Collection<FalconSeal> seals = roundState.getFalconSeals();
|
final Collection<FalconSeal> seals = roundState.getFalconSeals();
|
||||||
if (seals.isEmpty()) {
|
if (seals.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PqSealCache.instance().record(block.getHeader().getNumber(), pqOnchainHashOf(block), seals);
|
final Hash onchain = pqOnchainHashOf(block);
|
||||||
|
PqSealCache.instance().record(block.getHeader().getNumber(), onchain, seals);
|
||||||
|
// AERE ANCHOR V2: the extra scheme seals ride the same commits and go under the same key.
|
||||||
|
final java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> extras =
|
||||||
|
roundState.getExtraSeals();
|
||||||
|
if (!extras.isEmpty()) {
|
||||||
|
PqSealCache.instance().recordExtras(block.getHeader().getNumber(), onchain, extras);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -619,20 +662,54 @@ public class QbftRound {
|
|||||||
*/
|
*/
|
||||||
private java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> extraSealsFor(
|
private java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> extraSealsFor(
|
||||||
final QbftBlock block, final Hash commitHash) {
|
final QbftBlock block, final Hash commitHash) {
|
||||||
return org.hyperledger.besu.consensus.common.bft.HybridSealSupport.instance()
|
// ANCHOR PARENTS ONLY (2026-09-03, D-329): an SLH-DSA-128s signature costs seconds here; it is
|
||||||
.producer()
|
// only ever used in the next anchor's certificate, which is built over the anchor's parent. So
|
||||||
.sealsFor(block.getHeader().getNumber(), pqSealMessageFor(block, commitHash));
|
// the extras are signed only when the block being committed is the parent of an anchor height.
|
||||||
|
// The same predicate gates the enforcement (PqCommitEnforcement.anchorParentByConfig).
|
||||||
|
final long number = block.getHeader().getNumber();
|
||||||
|
if (!org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer.config()
|
||||||
|
.anchorAppliesAt(number + 1L)) {
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
|
return hybridExtrasOrEmpty(number, pqSealMessageFor(block, commitHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-334 (2026-09-03, mainnet 2800, 125 s of halted chain): the hybrid loader is lazy, so a
|
||||||
|
* registry the container could not read surfaced here, as an exception thrown INTO the QBFT
|
||||||
|
* state machine, at every anchor parent, on every armed node. Three such nodes plus one node
|
||||||
|
* restarting left five of nine. A broken hybrid configuration may cost this node its SLH-DSA
|
||||||
|
* contribution; it must never cost the node its vote. The loader still refuses to START on the
|
||||||
|
* same defect (QbftBesuControllerBuilder calls it at boot), so this catch is the second net.
|
||||||
|
*
|
||||||
|
* @param number the height being committed
|
||||||
|
* @param message the seal message
|
||||||
|
* @return the extras, or an empty list with an ERROR line when the hybrid support cannot sign
|
||||||
|
*/
|
||||||
|
static java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> hybridExtrasOrEmpty(
|
||||||
|
final long number, final Bytes32 message) {
|
||||||
|
try {
|
||||||
|
return org.hyperledger.besu.consensus.common.bft.HybridSealSupport.instance()
|
||||||
|
.producer()
|
||||||
|
.sealsFor(number, message);
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
LOG.error(
|
||||||
|
"AERE HIBRID: extras NOT signed at height {} - {}. This node keeps voting but contributes no"
|
||||||
|
+ " SLH-DSA seal until the hybrid configuration is repaired (D-334).",
|
||||||
|
number,
|
||||||
|
e.getMessage());
|
||||||
|
return java.util.List.of();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The exact bytes a PQ seal over this block signs; shared by Falcon and the hybrid extras,
|
/** The exact bytes a PQ seal over this block signs; shared by Falcon and the hybrid extras,
|
||||||
* so the two halves of a hybrid certificate can never drift onto different messages. */
|
* so the two halves of a hybrid certificate can never drift onto different messages. */
|
||||||
private Bytes32 pqSealMessageFor(final QbftBlock block, final Hash commitHash) {
|
private Bytes32 pqSealMessageFor(final QbftBlock block, final Hash commitHash) {
|
||||||
final long blockNumber = block.getHeader().getNumber();
|
// AERE D-311 (2026-09-02): the form is decided in ONE place, shared with the verifier
|
||||||
if (PqAnchorProducer.sealMessageIsAnchorForm(blockNumber)) {
|
// (MessageValidator.SubsequentMessageValidator). A second copy of this rule drifted once and
|
||||||
return PqAnchor.commitMessage(
|
// stopped a chain; see PqAnchorProducer.commitSealMessage.
|
||||||
PqAnchorProducer.config().chainId(), blockNumber, pqOnchainHashOf(block).getBytes());
|
return PqAnchorProducer.commitSealMessage(
|
||||||
}
|
block.getHeader().getNumber(), () -> pqOnchainHashOf(block).getBytes(), commitHash);
|
||||||
return Bytes32.wrap(commitHash.getBytes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<FalconSeal> falconSealFor(final QbftBlock block, final Hash commitHash) {
|
private Optional<FalconSeal> falconSealFor(final QbftBlock block, final Hash commitHash) {
|
||||||
|
|||||||
@ -212,6 +212,18 @@ public class RoundState {
|
|||||||
*
|
*
|
||||||
* @return the gossiped Falcon seals (possibly empty, never null)
|
* @return the gossiped Falcon seals (possibly empty, never null)
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* AERE ANCHOR V2: the extra (non-Falcon) scheme seals gossiped on the collected commit messages,
|
||||||
|
* raw and unverified, for the seal cache and the v2 certificate producer.
|
||||||
|
*
|
||||||
|
* @return the gossiped scheme seals (possibly empty, never null)
|
||||||
|
*/
|
||||||
|
public java.util.List<org.hyperledger.besu.consensus.common.bft.SchemeSeal> getExtraSeals() {
|
||||||
|
return commitMessages.values().stream()
|
||||||
|
.flatMap(cp -> cp.getSignedPayload().getPayload().getExtraSeals().stream())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
public Collection<FalconSeal> getFalconSeals() {
|
public Collection<FalconSeal> getFalconSeals() {
|
||||||
return commitMessages.values().stream()
|
return commitMessages.values().stream()
|
||||||
.map(cp -> cp.getSignedPayload().getPayload().getFalconSeal())
|
.map(cp -> cp.getSignedPayload().getPayload().getFalconSeal())
|
||||||
|
|||||||
@ -47,6 +47,11 @@ public class CommitValidator {
|
|||||||
private final ConsensusRoundIdentifier targetRound;
|
private final ConsensusRoundIdentifier targetRound;
|
||||||
private final Hash expectedDigest;
|
private final Hash expectedDigest;
|
||||||
private final Hash expectedCommitDigest;
|
private final Hash expectedCommitDigest;
|
||||||
|
// AERE D-311 (2026-09-02): the message the post-quantum seal must have signed. It is NOT always
|
||||||
|
// the committed-seal digest: once the anchor is armed the emitter signs the anchor form (see
|
||||||
|
// PqAnchorProducer.commitSealMessage). The upstream-shaped constructors keep the digest, so no
|
||||||
|
// existing caller changes behaviour; the production path (MessageValidator) passes the real one.
|
||||||
|
private final Hash expectedPqSealMessage;
|
||||||
// AERE full-PQ: optional height-gated enforcement. Null means "upstream behaviour", which is
|
// AERE full-PQ: optional height-gated enforcement. Null means "upstream behaviour", which is
|
||||||
// exactly what the pre-existing constructor supplies, so nothing changes for existing callers.
|
// exactly what the pre-existing constructor supplies, so nothing changes for existing callers.
|
||||||
private final PqCommitEnforcement pqEnforcement;
|
private final PqCommitEnforcement pqEnforcement;
|
||||||
@ -75,6 +80,33 @@ public class CommitValidator {
|
|||||||
targetRound,
|
targetRound,
|
||||||
expectedDigest,
|
expectedDigest,
|
||||||
expectedCommitDigest,
|
expectedCommitDigest,
|
||||||
|
expectedCommitDigest,
|
||||||
|
PqCommitEnforcement.fromSystemConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE D-311: the production constructor. Self-wires enforcement from the system configuration
|
||||||
|
* and takes the exact message the PQ seal signs at this height, computed by the caller with
|
||||||
|
* {@code PqAnchorProducer.commitSealMessage}, the same helper the emitter uses.
|
||||||
|
*
|
||||||
|
* @param validators the validators
|
||||||
|
* @param targetRound the target round
|
||||||
|
* @param expectedDigest the expected digest
|
||||||
|
* @param expectedCommitDigest the expected commit digest (ECDSA committed seal)
|
||||||
|
* @param expectedPqSealMessage the message the post-quantum seal must have signed
|
||||||
|
*/
|
||||||
|
public CommitValidator(
|
||||||
|
final Collection<Address> validators,
|
||||||
|
final ConsensusRoundIdentifier targetRound,
|
||||||
|
final Hash expectedDigest,
|
||||||
|
final Hash expectedCommitDigest,
|
||||||
|
final Hash expectedPqSealMessage) {
|
||||||
|
this(
|
||||||
|
validators,
|
||||||
|
targetRound,
|
||||||
|
expectedDigest,
|
||||||
|
expectedCommitDigest,
|
||||||
|
expectedPqSealMessage,
|
||||||
PqCommitEnforcement.fromSystemConfig());
|
PqCommitEnforcement.fromSystemConfig());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,10 +130,37 @@ public class CommitValidator {
|
|||||||
final Hash expectedDigest,
|
final Hash expectedDigest,
|
||||||
final Hash expectedCommitDigest,
|
final Hash expectedCommitDigest,
|
||||||
final PqCommitEnforcement pqEnforcement) {
|
final PqCommitEnforcement pqEnforcement) {
|
||||||
|
this(validators, targetRound, expectedDigest, expectedCommitDigest, expectedCommitDigest, pqEnforcement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** AERE D-311: the message the post-quantum seal must have signed; for the plumbing proof only. */
|
||||||
|
@com.google.common.annotations.VisibleForTesting
|
||||||
|
Hash expectedPqSealMessageForTesting() {
|
||||||
|
return expectedPqSealMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE D-311: the fully explicit constructor (tests and the production constructor above).
|
||||||
|
*
|
||||||
|
* @param validators the validators
|
||||||
|
* @param targetRound the target round
|
||||||
|
* @param expectedDigest the expected digest
|
||||||
|
* @param expectedCommitDigest the expected commit digest (ECDSA committed seal)
|
||||||
|
* @param expectedPqSealMessage the message the post-quantum seal must have signed
|
||||||
|
* @param pqEnforcement the enforcement, or null for upstream behaviour
|
||||||
|
*/
|
||||||
|
public CommitValidator(
|
||||||
|
final Collection<Address> validators,
|
||||||
|
final ConsensusRoundIdentifier targetRound,
|
||||||
|
final Hash expectedDigest,
|
||||||
|
final Hash expectedCommitDigest,
|
||||||
|
final Hash expectedPqSealMessage,
|
||||||
|
final PqCommitEnforcement pqEnforcement) {
|
||||||
this.validators = validators;
|
this.validators = validators;
|
||||||
this.targetRound = targetRound;
|
this.targetRound = targetRound;
|
||||||
this.expectedDigest = expectedDigest;
|
this.expectedDigest = expectedDigest;
|
||||||
this.expectedCommitDigest = expectedCommitDigest;
|
this.expectedCommitDigest = expectedCommitDigest;
|
||||||
|
this.expectedPqSealMessage = expectedPqSealMessage;
|
||||||
this.pqEnforcement = pqEnforcement;
|
this.pqEnforcement = pqEnforcement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +225,7 @@ public class CommitValidator {
|
|||||||
pqEnforcement.refusal(
|
pqEnforcement.refusal(
|
||||||
targetRound.getSequenceNumber(),
|
targetRound.getSequenceNumber(),
|
||||||
signedPayload.getAuthor(),
|
signedPayload.getAuthor(),
|
||||||
expectedCommitDigest,
|
expectedPqSealMessage,
|
||||||
payload.getFalconSeal(),
|
payload.getFalconSeal(),
|
||||||
payload.getExtraSeals());
|
payload.getExtraSeals());
|
||||||
if (refusal.isPresent()) {
|
if (refusal.isPresent()) {
|
||||||
|
|||||||
@ -0,0 +1,193 @@
|
|||||||
|
/*
|
||||||
|
* Copyright ConsenSys AG.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||||
|
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.qbft.core.validation;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.messagewrappers.Commit;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.messagewrappers.Prepare;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.messagewrappers.Proposal;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockInterface;
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
import org.hyperledger.besu.datatypes.Hash;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/** The Message validator. */
|
||||||
|
public class MessageValidator {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MessageValidator.class);
|
||||||
|
|
||||||
|
/** The Subsequent message validator. */
|
||||||
|
public static class SubsequentMessageValidator {
|
||||||
|
|
||||||
|
private final PrepareValidator prepareValidator;
|
||||||
|
private final CommitValidator commitValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new Subsequent message validator.
|
||||||
|
*
|
||||||
|
* @param validators the validators
|
||||||
|
* @param targetRound the target round
|
||||||
|
* @param proposalBlock the proposal block
|
||||||
|
* @param blockInterface the block interface
|
||||||
|
*/
|
||||||
|
public SubsequentMessageValidator(
|
||||||
|
final Collection<Address> validators,
|
||||||
|
final ConsensusRoundIdentifier targetRound,
|
||||||
|
final QbftBlock proposalBlock,
|
||||||
|
final QbftBlockInterface blockInterface) {
|
||||||
|
final QbftBlock commitBlock =
|
||||||
|
blockInterface.replaceRoundForCommitBlock(proposalBlock, targetRound.getRoundNumber());
|
||||||
|
prepareValidator = new PrepareValidator(validators, targetRound, proposalBlock.getHash());
|
||||||
|
// AERE D-311 (2026-09-02): the post-quantum seal on a commit signs the message that
|
||||||
|
// PqAnchorProducer.commitSealMessage names (the anchor form once the anchor is armed, the
|
||||||
|
// ECDSA committed-seal hash before), computed here exactly as the emitter computes it in
|
||||||
|
// QbftRound.pqSealMessageFor: same helper, same round-independent on-chain hash.
|
||||||
|
final Hash expectedPqSealMessage =
|
||||||
|
Hash.wrap(
|
||||||
|
PqAnchorProducer.commitSealMessage(
|
||||||
|
targetRound.getSequenceNumber(),
|
||||||
|
() -> blockInterface.replaceRoundForCommitBlock(proposalBlock, 0).getHash().getBytes(),
|
||||||
|
commitBlock.getHash()));
|
||||||
|
commitValidator =
|
||||||
|
new CommitValidator(
|
||||||
|
validators,
|
||||||
|
targetRound,
|
||||||
|
proposalBlock.getHash(),
|
||||||
|
commitBlock.getHash(),
|
||||||
|
expectedPqSealMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate.
|
||||||
|
*
|
||||||
|
* @param msg the Prepare payload msg
|
||||||
|
* @return the boolean
|
||||||
|
*/
|
||||||
|
public boolean validate(final Prepare msg) {
|
||||||
|
return prepareValidator.validate(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate.
|
||||||
|
*
|
||||||
|
* @param msg the Commit payload msg
|
||||||
|
* @return the boolean
|
||||||
|
*/
|
||||||
|
/** AERE D-311: the commit validator this wiring built; for the plumbing proof only. */
|
||||||
|
@com.google.common.annotations.VisibleForTesting
|
||||||
|
CommitValidator commitValidatorForTesting() {
|
||||||
|
return commitValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validate(final Commit msg) {
|
||||||
|
return commitValidator.validate(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The interface Subsequent message validator factory. */
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SubsequentMessageValidatorFactory {
|
||||||
|
/**
|
||||||
|
* Create subsequent message validator.
|
||||||
|
*
|
||||||
|
* @param proposalBlock the proposal block
|
||||||
|
* @return the subsequent message validator
|
||||||
|
*/
|
||||||
|
SubsequentMessageValidator create(QbftBlock proposalBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SubsequentMessageValidatorFactory subsequentMessageValidatorFactory;
|
||||||
|
private final ProposalValidator proposalValidator;
|
||||||
|
|
||||||
|
private Optional<SubsequentMessageValidator> subsequentMessageValidator = Optional.empty();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new Message validator.
|
||||||
|
*
|
||||||
|
* @param subsequentMessageValidatorFactory the subsequent message validator factory
|
||||||
|
* @param proposalValidator the proposal validator
|
||||||
|
*/
|
||||||
|
public MessageValidator(
|
||||||
|
final SubsequentMessageValidatorFactory subsequentMessageValidatorFactory,
|
||||||
|
final ProposalValidator proposalValidator) {
|
||||||
|
this.subsequentMessageValidatorFactory = subsequentMessageValidatorFactory;
|
||||||
|
this.proposalValidator = proposalValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate proposal payload.
|
||||||
|
*
|
||||||
|
* @param msg the Proposal payload msg
|
||||||
|
* @return the boolean
|
||||||
|
*/
|
||||||
|
public boolean validateProposal(final Proposal msg) {
|
||||||
|
return validateProposal(msg, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate proposal payload without block validation.
|
||||||
|
*
|
||||||
|
* @param msg the Proposal payload msg
|
||||||
|
* @return whether the proposal is valid
|
||||||
|
*/
|
||||||
|
public boolean validateProposalWithoutBlockValidation(final Proposal msg) {
|
||||||
|
return validateProposal(msg, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean validateProposal(final Proposal msg, final boolean validateBlock) {
|
||||||
|
if (subsequentMessageValidator.isPresent()) {
|
||||||
|
LOG.info("Received subsequent Proposal for current round, discarding.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean result =
|
||||||
|
validateBlock
|
||||||
|
? proposalValidator.validate(msg)
|
||||||
|
: proposalValidator.validateWithoutBlockValidation(msg);
|
||||||
|
if (result) {
|
||||||
|
subsequentMessageValidator =
|
||||||
|
Optional.of(subsequentMessageValidatorFactory.create(msg.getBlock()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Prepare payload.
|
||||||
|
*
|
||||||
|
* @param msg the Prepare msg
|
||||||
|
* @return the boolean
|
||||||
|
*/
|
||||||
|
public boolean validatePrepare(final Prepare msg) {
|
||||||
|
return subsequentMessageValidator.map(pv -> pv.validate(msg)).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate commit payload.
|
||||||
|
*
|
||||||
|
* @param msg the Commit payload
|
||||||
|
* @return the boolean
|
||||||
|
*/
|
||||||
|
public boolean validateCommit(final Commit msg) {
|
||||||
|
return subsequentMessageValidator.map(cv -> cv.validate(msg)).orElse(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,12 +26,14 @@ import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
|
|||||||
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
|
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
|
||||||
import org.hyperledger.besu.consensus.common.bft.SealScheme;
|
import org.hyperledger.besu.consensus.common.bft.SealScheme;
|
||||||
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
|
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||||
import org.hyperledger.besu.datatypes.Address;
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
import org.hyperledger.besu.datatypes.Hash;
|
import org.hyperledger.besu.datatypes.Hash;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.LongPredicate;
|
||||||
|
|
||||||
import org.apache.tuweni.bytes.Bytes;
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
|
||||||
@ -110,6 +112,7 @@ public final class PqCommitEnforcement {
|
|||||||
// AERE HIBRID: null in Falcon-only mode, which is every node today.
|
// AERE HIBRID: null in Falcon-only mode, which is every node today.
|
||||||
private final PqSchemeSchedule schemeSchedule;
|
private final PqSchemeSchedule schemeSchedule;
|
||||||
private final HybridSignerRegistry hybridRegistry;
|
private final HybridSignerRegistry hybridRegistry;
|
||||||
|
private final LongPredicate extrasRequiredAt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param armedFromBlock first block height (inclusive) at which enforcement applies; use
|
* @param armedFromBlock first block height (inclusive) at which enforcement applies; use
|
||||||
@ -146,10 +149,41 @@ public final class PqCommitEnforcement {
|
|||||||
final PqSignerRegistry registry,
|
final PqSignerRegistry registry,
|
||||||
final PqSchemeSchedule schemeSchedule,
|
final PqSchemeSchedule schemeSchedule,
|
||||||
final HybridSignerRegistry hybridRegistry) {
|
final HybridSignerRegistry hybridRegistry) {
|
||||||
|
this(armedFromBlock, registry, schemeSchedule, hybridRegistry, anchorParentByConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ANCHOR PARENTS ONLY (2026-09-03, D-329). An SLH-DSA-SHA2-128s signature costs seconds in this
|
||||||
|
* runtime; demanded on EVERY commit it stretched the testnet from 0.5 s to 4 s per block. The
|
||||||
|
* extra seal is only ever USED in the certificate of the next anchor, which is built from the
|
||||||
|
* seals heard over the anchor's PARENT. So the extra scheme seals are required (and emitted, see
|
||||||
|
* QbftRound) only on commits over a block whose successor is an anchor height. Falcon stays on
|
||||||
|
* every commit, exactly as before.
|
||||||
|
*
|
||||||
|
* @return the predicate "the block at this height is the parent of an anchor", from the live
|
||||||
|
* anchor configuration
|
||||||
|
*/
|
||||||
|
public static LongPredicate anchorParentByConfig() {
|
||||||
|
return h -> PqAnchorProducer.config().anchorAppliesAt(h + 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full constructor.
|
||||||
|
*
|
||||||
|
* @param extrasRequiredAt at which heights the extra (non-Falcon) scheme seals are demanded on a
|
||||||
|
* commit; production passes {@link #anchorParentByConfig()}
|
||||||
|
*/
|
||||||
|
public PqCommitEnforcement(
|
||||||
|
final long armedFromBlock,
|
||||||
|
final PqSignerRegistry registry,
|
||||||
|
final PqSchemeSchedule schemeSchedule,
|
||||||
|
final HybridSignerRegistry hybridRegistry,
|
||||||
|
final LongPredicate extrasRequiredAt) {
|
||||||
this.armedFromBlock = armedFromBlock;
|
this.armedFromBlock = armedFromBlock;
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
this.schemeSchedule = schemeSchedule;
|
this.schemeSchedule = schemeSchedule;
|
||||||
this.hybridRegistry = hybridRegistry;
|
this.hybridRegistry = hybridRegistry;
|
||||||
|
this.extrasRequiredAt = extrasRequiredAt;
|
||||||
if ((schemeSchedule == null) != (hybridRegistry == null)) {
|
if ((schemeSchedule == null) != (hybridRegistry == null)) {
|
||||||
// Half a hybrid configuration is the shape that starts a node believing it enforces
|
// Half a hybrid configuration is the shape that starts a node believing it enforces
|
||||||
// something it does not. Refuse at construction, the same stance as every other AERE gate.
|
// something it does not. Refuse at construction, the same stance as every other AERE gate.
|
||||||
@ -198,7 +232,10 @@ public final class PqCommitEnforcement {
|
|||||||
final Optional<FalconSeal> seal,
|
final Optional<FalconSeal> seal,
|
||||||
final List<SchemeSeal> extraSeals) {
|
final List<SchemeSeal> extraSeals) {
|
||||||
final Optional<String> falconVerdict = falconRefusal(height, author, commitDigest, seal);
|
final Optional<String> falconVerdict = falconRefusal(height, author, commitDigest, seal);
|
||||||
if (falconVerdict.isPresent() || !armedAt(height) || schemeSchedule == null) {
|
if (falconVerdict.isPresent()
|
||||||
|
|| !armedAt(height)
|
||||||
|
|| schemeSchedule == null
|
||||||
|
|| !extrasRequiredAt.test(height)) {
|
||||||
return falconVerdict;
|
return falconVerdict;
|
||||||
}
|
}
|
||||||
return hybridRefusal(height, author, commitDigest, seal.orElseThrow(), extraSeals);
|
return hybridRefusal(height, author, commitDigest, seal.orElseThrow(), extraSeals);
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to Hyperledger Besu.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
|
||||||
|
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations under the License.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.qbft.core.statemachine;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.HybridSealSupport;
|
||||||
|
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-334 (2026-09-03): a hybrid configuration the node cannot read must cost the node its SLH-DSA
|
||||||
|
* contribution, never its vote. The loader throws (the defect is real and named); the state
|
||||||
|
* machine's helper returns an empty list instead of letting that exception reach the QBFT event
|
||||||
|
* loop. Both halves are asserted, so the test cannot pass by the configuration being silently
|
||||||
|
* ignored.
|
||||||
|
*/
|
||||||
|
public class PqHybridExtrasNeverThrowTest {
|
||||||
|
|
||||||
|
private static final String[] PROPS = {
|
||||||
|
HybridSealSupport.PROPERTY_SCHEDULE,
|
||||||
|
HybridSealSupport.PROPERTY_REGISTRY,
|
||||||
|
HybridSealSupport.PROPERTY_ATTACH_BLOCK
|
||||||
|
};
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void plantAnUnreadableRegistry() {
|
||||||
|
HybridSealSupport.resetForTesting();
|
||||||
|
System.setProperty(HybridSealSupport.PROPERTY_SCHEDULE, "10:falcon-512+slh-dsa-sha2-128s");
|
||||||
|
System.setProperty(
|
||||||
|
HybridSealSupport.PROPERTY_REGISTRY, "/nonexistent/aere-d334/hibrid-1.properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void cleanUp() {
|
||||||
|
for (final String p : PROPS) {
|
||||||
|
System.clearProperty(p);
|
||||||
|
}
|
||||||
|
HybridSealSupport.resetForTesting();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theLoaderNamesTheDefect() {
|
||||||
|
assertThatThrownBy(HybridSealSupport::instance)
|
||||||
|
.isInstanceOf(IllegalStateException.class)
|
||||||
|
.hasMessageContaining("AERE-PQC-HYBRID-CONF-04");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theStateMachineHelperReturnsEmptyInsteadOfThrowing() {
|
||||||
|
assertThat(QbftRound.hybridExtrasOrEmpty(31L, Bytes32.ZERO)).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to Aere Network.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
|
||||||
|
* compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||||
|
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
* implied. See the License for the specific language governing permissions and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.qbft.core.statemachine;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqSealCache;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.RoundTimer;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.network.QbftMessageTransmitter;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.payload.MessageFactory;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCreator;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockHeader;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockInterface;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftMinedBlockObserver;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftProtocolSchedule;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.validation.MessageValidator;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.validation.QbftBlockHeaderTestFixture;
|
||||||
|
import org.hyperledger.besu.cryptoservices.NodeKey;
|
||||||
|
import org.hyperledger.besu.cryptoservices.NodeKeyUtils;
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
import org.hyperledger.besu.datatypes.Hash;
|
||||||
|
import org.hyperledger.besu.util.Subscribers;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* D-339 (2026-09-04): a Commit teaches this node the seals of its block even when this node holds NO
|
||||||
|
* proposal for that round.
|
||||||
|
*
|
||||||
|
* <p>The measured defect, on chain 2800 the same evening: anchor proposers kept refusing with "this
|
||||||
|
* node holds 1 valid eligible Falcon seal ... 0 rejected" - they were not rejecting seals, they had
|
||||||
|
* never heard them. The cache was fed only from a round that held the proposed block, so a node that
|
||||||
|
* missed the proposal of the parent's round collected the Commits and dropped their seals; it then
|
||||||
|
* could not build the next anchor's certificate, the round expired, and every 32nd height paid the
|
||||||
|
* round-change timeout (eleven of twelve anchors before this fix).
|
||||||
|
*
|
||||||
|
* <p>What is pinned here: with no proposal in the round, a Commit's Falcon seal AND its hybrid extra
|
||||||
|
* seals land in the cache under the digest the Commit names; and the negative control, a Commit
|
||||||
|
* carrying no seals, leaves the cache empty.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
public class PqSealsWithoutProposalTest {
|
||||||
|
|
||||||
|
private final NodeKey nodeKey = NodeKeyUtils.generate();
|
||||||
|
private final Address localAddress = Address.extract(nodeKey.getPublicKey());
|
||||||
|
private final NodeKey peerKey = NodeKeyUtils.generate();
|
||||||
|
private final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(7, 0);
|
||||||
|
private final Subscribers<QbftMinedBlockObserver> subscribers = Subscribers.create();
|
||||||
|
private final Hash digest = Hash.hash(Bytes.fromHexString("0xdeadbeef"));
|
||||||
|
private final FalconSeal falcon = new FalconSeal(4, Bytes.fromHexString("0x29aabbcc"));
|
||||||
|
private final SchemeSeal slh = new SchemeSeal((byte) 0x02, 4, Bytes.fromHexString("0x0102030405"));
|
||||||
|
|
||||||
|
private MessageFactory localFactory;
|
||||||
|
private MessageFactory peerFactory;
|
||||||
|
|
||||||
|
@Mock private org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec blockEncoder;
|
||||||
|
|
||||||
|
@Mock private QbftProtocolSchedule protocolSchedule;
|
||||||
|
@Mock private QbftMessageTransmitter transmitter;
|
||||||
|
@Mock private MessageValidator messageValidator;
|
||||||
|
@Mock private RoundTimer roundTimer;
|
||||||
|
@Mock private QbftBlockCreator blockCreator;
|
||||||
|
@Mock private QbftBlockInterface blockInterface;
|
||||||
|
|
||||||
|
private final QbftBlockHeader parentHeader =
|
||||||
|
new QbftBlockHeaderTestFixture().number(6).buildHeader();
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
PqSealCache.instance().clear();
|
||||||
|
localFactory = new MessageFactory(nodeKey, blockEncoder);
|
||||||
|
peerFactory = new MessageFactory(peerKey, blockEncoder);
|
||||||
|
when(messageValidator.validateCommit(org.mockito.ArgumentMatchers.any())).thenReturn(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void clean() {
|
||||||
|
PqSealCache.instance().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private QbftRound roundWithoutProposal() {
|
||||||
|
final RoundState roundState = new RoundState(roundIdentifier, 3, messageValidator);
|
||||||
|
assertThat(roundState.getProposedBlock()).isEmpty();
|
||||||
|
return new QbftRound(
|
||||||
|
roundState,
|
||||||
|
blockCreator,
|
||||||
|
blockInterface,
|
||||||
|
protocolSchedule,
|
||||||
|
subscribers,
|
||||||
|
nodeKey,
|
||||||
|
localAddress,
|
||||||
|
localFactory,
|
||||||
|
transmitter,
|
||||||
|
roundTimer,
|
||||||
|
parentHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aCommitWithoutAProposalStillTeachesItsSeals() {
|
||||||
|
final QbftRound round = roundWithoutProposal();
|
||||||
|
round.handleCommitMessage(
|
||||||
|
peerFactory.createCommit(
|
||||||
|
roundIdentifier,
|
||||||
|
digest,
|
||||||
|
peerKey.sign(org.apache.tuweni.bytes.Bytes32.ZERO),
|
||||||
|
Optional.of(falcon),
|
||||||
|
List.of(slh)));
|
||||||
|
|
||||||
|
assertThat(PqSealCache.instance().sealsFor(roundIdentifier.getSequenceNumber(), digest))
|
||||||
|
.containsExactly(falcon);
|
||||||
|
assertThat(PqSealCache.instance().extrasFor(roundIdentifier.getSequenceNumber(), digest))
|
||||||
|
.containsExactly(slh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aCommitWithoutSealsLeavesTheCacheEmpty() {
|
||||||
|
// the negative control: the path must not write an entry just because a Commit arrived
|
||||||
|
final QbftRound round = roundWithoutProposal();
|
||||||
|
round.handleCommitMessage(
|
||||||
|
peerFactory.createCommit(roundIdentifier, digest, peerKey.sign(org.apache.tuweni.bytes.Bytes32.ZERO)));
|
||||||
|
|
||||||
|
assertThat(PqSealCache.instance().sealsFor(roundIdentifier.getSequenceNumber(), digest))
|
||||||
|
.isEmpty();
|
||||||
|
assertThat(PqSealCache.instance().extrasFor(roundIdentifier.getSequenceNumber(), digest))
|
||||||
|
.isEmpty();
|
||||||
|
assertThat(PqSealCache.instance().entryCount()).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sealsLandUnderTheDigestTheCommitNames() {
|
||||||
|
// a seal must never be filed under some other block's hash: the producer looks up by the
|
||||||
|
// parent's on-chain hash, and a mis-filed seal would be an invisible way to poison a lookup
|
||||||
|
final QbftRound round = roundWithoutProposal();
|
||||||
|
round.handleCommitMessage(
|
||||||
|
peerFactory.createCommit(
|
||||||
|
roundIdentifier, digest, peerKey.sign(org.apache.tuweni.bytes.Bytes32.ZERO), Optional.of(falcon), List.of(slh)));
|
||||||
|
|
||||||
|
final Hash altul = Hash.hash(Bytes.fromHexString("0xfeedface"));
|
||||||
|
assertThat(PqSealCache.instance().sealsFor(roundIdentifier.getSequenceNumber(), altul)).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to the Aere Network.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
|
||||||
|
* compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||||
|
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
* implied. See the License for the specific language governing permissions and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.qbft.core.validation;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealScheme;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.messagewrappers.Commit;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec;
|
||||||
|
import org.hyperledger.besu.crypto.SECPSignature;
|
||||||
|
import org.hyperledger.besu.crypto.SecureRandomProvider;
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
import org.hyperledger.besu.datatypes.Hash;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE D-311 (2026-09-02). Found by the public testnet 28001 on its first day, with the options of
|
||||||
|
* fleet 2800: at {@code aere.pq.commitPq.forkBlock} every validator refused every commit ("post-quantum
|
||||||
|
* seal of index i does NOT verify over the commit digest") and the chain stopped. The emitter signs
|
||||||
|
* the ANCHOR form of the commit message as soon as the anchor is armed (on 2800 since 13,014,000);
|
||||||
|
* the verifier checked the ECDSA committed-seal digest. Two copies of one rule, and they drifted. The
|
||||||
|
* F95 rehearsal could not see it: its kit runs without the anchor armed.
|
||||||
|
*
|
||||||
|
* <p>What this proves, with real Falcon-512 signatures and nothing mocked on the cryptographic path:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>anchor armed: a seal made exactly as the emitter makes it (over {@code
|
||||||
|
* PqAnchorProducer.commitSealMessage}) COUNTS through the production message;
|
||||||
|
* <li>CONTROL NEGATIV: the SAME seal fed to a validator that still verifies over the commit digest
|
||||||
|
* (the pre-D-311 behaviour, kept by the upstream-shaped constructors) is REFUZAT. This is the
|
||||||
|
* bug itself, reproduced; if this test ever goes green the fix has been undone somewhere else;
|
||||||
|
* <li>anchor armed, an emitter that kept signing the digest is REFUZAT by the fixed verifier;
|
||||||
|
* <li>anchor never armed: the message IS the commit digest, so old and new verifiers agree.
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class CommitValidatorAnchorFormTest {
|
||||||
|
private static final int VALIDATOR_COUNT = 3;
|
||||||
|
private static final long CHAIN_ID = 28001L;
|
||||||
|
private static final long HEIGHT = 5_000L;
|
||||||
|
private final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(HEIGHT, 0);
|
||||||
|
/** The ECDSA committed-seal digest (round-specific). */
|
||||||
|
private final Hash commitDigest = Hash.fromHexStringLenient("0x1");
|
||||||
|
/** The round-independent on-chain hash of the same block (round forced to 0). */
|
||||||
|
private final Hash onchainHash = Hash.fromHexStringLenient("0x2");
|
||||||
|
private QbftNodeList validators;
|
||||||
|
private @Mock QbftBlockCodec qbftBlockCodec;
|
||||||
|
private final SecureRandom random = SecureRandomProvider.createSecureRandom();
|
||||||
|
private final Map<Integer, Address> bindings = new HashMap<>();
|
||||||
|
private final Map<Integer, byte[]> cheiPublice = new HashMap<>();
|
||||||
|
private final Map<Integer, SealScheme.PrivateHandle> cheiPrivate = new HashMap<>();
|
||||||
|
|
||||||
|
private final PqSignerRegistry registry =
|
||||||
|
new PqSignerRegistry() {
|
||||||
|
@Override
|
||||||
|
public Address addressForIndexAtHistoric(final long blockNumber, final int validatorIndex) {
|
||||||
|
return bindings.get(validatorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address addressForIndexAtOwnHead(final long blockNumber, final int validatorIndex) {
|
||||||
|
return bindings.get(validatorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verifyAtHistoric(
|
||||||
|
final long blockNumber,
|
||||||
|
final int validatorIndex,
|
||||||
|
final Bytes message,
|
||||||
|
final Bytes signature) {
|
||||||
|
return verifyAtOwnHead(blockNumber, validatorIndex, message, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verifyAtOwnHead(
|
||||||
|
final long blockNumber,
|
||||||
|
final int validatorIndex,
|
||||||
|
final Bytes message,
|
||||||
|
final Bytes signature) {
|
||||||
|
final byte[] pk = cheiPublice.get(validatorIndex);
|
||||||
|
return pk != null
|
||||||
|
&& SealSchemes.FALCON_512.verifyRaw(pk, message.toArray(), signature.toArray());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
validators = QbftNodeList.createNodes(VALIDATOR_COUNT, qbftBlockCodec);
|
||||||
|
for (int i = 0; i < VALIDATOR_COUNT; i++) {
|
||||||
|
final SealScheme.GeneratedPair pereche = SealSchemes.FALCON_512.generate(random);
|
||||||
|
bindings.put(i, validators.getNode(i).getAddress());
|
||||||
|
cheiPublice.put(i, pereche.publicRegistryForm());
|
||||||
|
cheiPrivate.put(i, pereche.privateKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void forgetAnchorConfig() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void armAnchorAt(final long anchorBlock) {
|
||||||
|
PqAnchorProducer.useConfigForTesting(
|
||||||
|
new PqAnchorConfig(
|
||||||
|
CHAIN_ID, anchorBlock, Map.of(anchorBlock, 0), OptionalInt.empty(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exactly what the emitter (QbftRound.pqSealMessageFor) signs at HEIGHT. */
|
||||||
|
private Bytes32 emitterMessage() {
|
||||||
|
return PqAnchorProducer.commitSealMessage(HEIGHT, onchainHash.getBytes(), commitDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The fixed production shape: the verifier is handed the same message the emitter signs. */
|
||||||
|
private CommitValidator verificatorReparat(final Bytes32 expectedPqSealMessage) {
|
||||||
|
return new CommitValidator(
|
||||||
|
validators.getNodeAddresses(),
|
||||||
|
round,
|
||||||
|
commitDigest,
|
||||||
|
commitDigest,
|
||||||
|
Hash.wrap(expectedPqSealMessage),
|
||||||
|
new PqCommitEnforcement(HEIGHT, registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The pre-D-311 shape: enforcement verifies over the commit digest, whatever the emitter signed. */
|
||||||
|
private CommitValidator verificatorVechi() {
|
||||||
|
return new CommitValidator(
|
||||||
|
validators.getNodeAddresses(),
|
||||||
|
round,
|
||||||
|
commitDigest,
|
||||||
|
commitDigest,
|
||||||
|
new PqCommitEnforcement(HEIGHT, registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Commit commitSealedOver(final int nod, final Bytes32 message) {
|
||||||
|
final SECPSignature ecdsa =
|
||||||
|
validators.getNode(nod).getNodeKey().sign(Bytes32.wrap(commitDigest.getBytes()));
|
||||||
|
final byte[] sig =
|
||||||
|
SealSchemes.FALCON_512.sign(cheiPrivate.get(nod), message.toArray()).orElseThrow();
|
||||||
|
return validators
|
||||||
|
.getMessageFactory(nod)
|
||||||
|
.createCommit(round, commitDigest, ecdsa, Optional.of(new FalconSeal(nod, Bytes.wrap(sig))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anchorArmedTheMessageIsTheAnchorFormNotTheDigest() {
|
||||||
|
armAnchorAt(HEIGHT);
|
||||||
|
assertThat(PqAnchorProducer.sealMessageIsAnchorForm(HEIGHT)).isTrue();
|
||||||
|
assertThat(emitterMessage()).isNotEqualTo(Bytes32.wrap(commitDigest.getBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anchorArmedSealMadeLikeTheEmitterCountsThroughTheFixedVerifier() {
|
||||||
|
armAnchorAt(HEIGHT);
|
||||||
|
final Bytes32 message = emitterMessage();
|
||||||
|
assertThat(verificatorReparat(message).validate(commitSealedOver(0, message))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void controlNegativTheOldVerifierRefusesTheVerySameSeal() {
|
||||||
|
// This is D-311 itself: the seal the emitter really produces, judged over the commit digest.
|
||||||
|
// On the testnet this line was the whole chain stopping at the fork height.
|
||||||
|
armAnchorAt(HEIGHT);
|
||||||
|
final Bytes32 message = emitterMessage();
|
||||||
|
assertThat(verificatorVechi().validate(commitSealedOver(0, message))).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anchorArmedAnEmitterStillSigningTheDigestIsRefused() {
|
||||||
|
armAnchorAt(HEIGHT);
|
||||||
|
final Bytes32 sealedOverDigest = Bytes32.wrap(commitDigest.getBytes());
|
||||||
|
assertThat(verificatorReparat(emitterMessage()).validate(commitSealedOver(1, sealedOverDigest)))
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anchorNeverArmedTheMessageIsTheDigestAndBothVerifiersAgree() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(PqAnchorConfig.never(CHAIN_ID));
|
||||||
|
final Bytes32 message = emitterMessage();
|
||||||
|
assertThat(message).isEqualTo(Bytes32.wrap(commitDigest.getBytes()));
|
||||||
|
assertThat(verificatorReparat(message).validate(commitSealedOver(2, message))).isTrue();
|
||||||
|
assertThat(verificatorVechi().validate(commitSealedOver(2, message))).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to the Aere Network.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
|
||||||
|
* compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||||
|
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
* implied. See the License for the specific language governing permissions and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.qbft.core.validation;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.blockcreation.PqAnchorProducer;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockInterface;
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
import org.hyperledger.besu.datatypes.Hash;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE D-311, the PRODUCTION wiring. {@link CommitValidatorAnchorFormTest} proves the verifier accepts
|
||||||
|
* the emitter's seal when it is handed the emitter's message; this proves that {@link
|
||||||
|
* MessageValidator.SubsequentMessageValidator}, the only production caller, hands it exactly that
|
||||||
|
* message: the anchor form over the ROUND-INDEPENDENT on-chain hash (round forced to 0) once the
|
||||||
|
* anchor is armed, and the plain committed-seal digest before, computed through the one shared helper
|
||||||
|
* and touching the block interface for round 0 only when the anchor form needs it.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class MessageValidatorAnchorFormPlumbingTest {
|
||||||
|
private static final long CHAIN_ID = 28001L;
|
||||||
|
private static final long HEIGHT = 7_000L;
|
||||||
|
private static final int ROUND = 2;
|
||||||
|
private final ConsensusRoundIdentifier round = new ConsensusRoundIdentifier(HEIGHT, ROUND);
|
||||||
|
private final Hash proposalHash = Hash.fromHexStringLenient("0x11");
|
||||||
|
private final Hash commitHash = Hash.fromHexStringLenient("0x22");
|
||||||
|
private final Hash onchainHash = Hash.fromHexStringLenient("0x33");
|
||||||
|
|
||||||
|
private @Mock QbftBlockInterface blockInterface;
|
||||||
|
private @Mock QbftBlock proposalBlock;
|
||||||
|
private @Mock QbftBlock commitBlock;
|
||||||
|
private @Mock QbftBlock roundZeroBlock;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
when(proposalBlock.getHash()).thenReturn(proposalHash);
|
||||||
|
when(blockInterface.replaceRoundForCommitBlock(proposalBlock, ROUND)).thenReturn(commitBlock);
|
||||||
|
when(commitBlock.getHash()).thenReturn(commitHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void forgetAnchorConfig() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MessageValidator.SubsequentMessageValidator build() {
|
||||||
|
return new MessageValidator.SubsequentMessageValidator(
|
||||||
|
List.of(Address.fromHexString("0x1")), round, proposalBlock, blockInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anchorArmedTheVerifierIsHandedTheAnchorFormOverTheRoundZeroHash() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(
|
||||||
|
new PqAnchorConfig(CHAIN_ID, HEIGHT, Map.of(HEIGHT, 0), OptionalInt.empty(), false));
|
||||||
|
when(blockInterface.replaceRoundForCommitBlock(proposalBlock, 0)).thenReturn(roundZeroBlock);
|
||||||
|
when(roundZeroBlock.getHash()).thenReturn(onchainHash);
|
||||||
|
|
||||||
|
final Hash handed = build().commitValidatorForTesting().expectedPqSealMessageForTesting();
|
||||||
|
|
||||||
|
assertThat(handed)
|
||||||
|
.isEqualTo(Hash.wrap(PqAnchor.commitMessage(CHAIN_ID, HEIGHT, onchainHash.getBytes())));
|
||||||
|
assertThat(handed).isNotEqualTo(commitHash);
|
||||||
|
verify(blockInterface).replaceRoundForCommitBlock(proposalBlock, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anchorNeverArmedTheVerifierIsHandedTheCommitDigestAndRoundZeroIsNeverAsked() {
|
||||||
|
PqAnchorProducer.useConfigForTesting(PqAnchorConfig.never(CHAIN_ID));
|
||||||
|
|
||||||
|
final Hash handed = build().commitValidatorForTesting().expectedPqSealMessageForTesting();
|
||||||
|
|
||||||
|
assertThat(handed).isEqualTo(commitHash);
|
||||||
|
verify(blockInterface, never()).replaceRoundForCommitBlock(any(), eq(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -111,7 +111,8 @@ public class PqHybridEnforcementTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PqCommitEnforcement hibrid() {
|
private PqCommitEnforcement hibrid() {
|
||||||
return new PqCommitEnforcement(H_ARMARE, registruFalcon(), orar, registry);
|
// every height counts as an anchor parent here, so the refusals below keep their meaning
|
||||||
|
return new PqCommitEnforcement(H_ARMARE, registruFalcon(), orar, registry, h -> true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private FalconSeal sigiliuFalcon(final int index) {
|
private FalconSeal sigiliuFalcon(final int index) {
|
||||||
@ -292,4 +293,20 @@ public class PqHybridEnforcementTest {
|
|||||||
assertThat(registry.address(0)).isPresent();
|
assertThat(registry.address(0)).isPresent();
|
||||||
assertThat(Map.of()).isEmpty();
|
assertThat(Map.of()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** D-329 (2026-09-03): off the anchor parents the extra seals are neither demanded nor emitted. */
|
||||||
|
@Test
|
||||||
|
void offTheAnchorParentsACommitWithoutExtrasStillCounts() {
|
||||||
|
final PqCommitEnforcement pe =
|
||||||
|
new PqCommitEnforcement(H_ARMARE, registruFalcon(), orar, registry, h -> false);
|
||||||
|
final FalconSeal falcon = sigiliuFalcon(0);
|
||||||
|
assertThat(pe.refusal(H_HIBRID, VALIDATOR_0, DIGEST, java.util.Optional.of(falcon), java.util.List.of()))
|
||||||
|
.describedAs("no extras demanded where the successor is not an anchor")
|
||||||
|
.isEmpty();
|
||||||
|
final PqCommitEnforcement peOn =
|
||||||
|
new PqCommitEnforcement(H_ARMARE, registruFalcon(), orar, registry, h -> true);
|
||||||
|
assertThat(peOn.refusal(H_HIBRID, VALIDATOR_0, DIGEST, java.util.Optional.of(falcon), java.util.List.of()))
|
||||||
|
.describedAs("the same commit IS refused where the successor is an anchor")
|
||||||
|
.isPresent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import static org.hyperledger.besu.consensus.common.bft.Vote.DROP_BYTE_VALUE;
|
|||||||
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
||||||
import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec;
|
import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorV2;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
|
||||||
import org.hyperledger.besu.consensus.common.bft.Vote;
|
import org.hyperledger.besu.consensus.common.bft.Vote;
|
||||||
import org.hyperledger.besu.consensus.common.validator.VoteType;
|
import org.hyperledger.besu.consensus.common.validator.VoteType;
|
||||||
import org.hyperledger.besu.crypto.SECPSignature;
|
import org.hyperledger.besu.crypto.SECPSignature;
|
||||||
@ -123,18 +125,43 @@ public class QbftExtraDataCodec extends BftExtraDataCodec {
|
|||||||
// Backward-tolerant: older/exclude/no-certificate encodings without this element decode to an
|
// Backward-tolerant: older/exclude/no-certificate encodings without this element decode to an
|
||||||
// empty list.
|
// empty list.
|
||||||
final List<FalconSeal> falconSeals;
|
final List<FalconSeal> falconSeals;
|
||||||
|
final List<SchemeSeal> hybridSeals;
|
||||||
if (!rlpInput.isEndOfCurrentList()) {
|
if (!rlpInput.isEndOfCurrentList()) {
|
||||||
falconSeals =
|
// AERE ANCHOR V2 (2026-09-03): the sixth element is EITHER the v1 Falcon list
|
||||||
rlpInput.readList(
|
// RLP[[idx,sig],...] OR the scheme-tagged v2 certificate RLP[2,[[scheme,idx,sig],...]].
|
||||||
rlp -> {
|
// They cannot be confused: v1 opens with a LIST, v2 with the SCALAR 2. The raw bytes are
|
||||||
rlp.enterList();
|
// taken first so the v2 decoder can enforce its canonical round trip on exactly what the
|
||||||
final int idx = rlp.readIntScalar();
|
// header carried; the codec's own round trip (gate 2 below) then covers both forms.
|
||||||
final Bytes sig = rlp.readBytes();
|
final Bytes certificateRaw = rlpInput.readAsRlp().raw();
|
||||||
rlp.leaveList();
|
final RLPInput probe = new BytesValueRLPInput(certificateRaw, false);
|
||||||
return new FalconSeal(idx, sig);
|
probe.enterList();
|
||||||
});
|
final boolean v2 = !probe.isEndOfCurrentList() && !probe.nextIsList();
|
||||||
|
if (v2) {
|
||||||
|
falconSeals = Collections.emptyList();
|
||||||
|
try {
|
||||||
|
hybridSeals = PqAnchorV2.decode(certificateRaw);
|
||||||
|
} catch (final RuntimeException e) {
|
||||||
|
throw new RLPException(
|
||||||
|
"AERE ANCHOR V2: the header's certificate does not decode as a canonical v2"
|
||||||
|
+ " certificate: "
|
||||||
|
+ e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hybridSeals = Collections.emptyList();
|
||||||
|
falconSeals =
|
||||||
|
new BytesValueRLPInput(certificateRaw, false)
|
||||||
|
.readList(
|
||||||
|
rlp -> {
|
||||||
|
rlp.enterList();
|
||||||
|
final int idx = rlp.readIntScalar();
|
||||||
|
final Bytes sig = rlp.readBytes();
|
||||||
|
rlp.leaveList();
|
||||||
|
return new FalconSeal(idx, sig);
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
falconSeals = Collections.emptyList();
|
falconSeals = Collections.emptyList();
|
||||||
|
hybridSeals = Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// AERE FIX-MALEABILITATE-BLOC (2026-08-01), gate 1 of 2: the leave is STRICT.
|
// AERE FIX-MALEABILITATE-BLOC (2026-08-01), gate 1 of 2: the leave is STRICT.
|
||||||
@ -150,7 +177,7 @@ public class QbftExtraDataCodec extends BftExtraDataCodec {
|
|||||||
rlpInput.leaveList();
|
rlpInput.leaveList();
|
||||||
|
|
||||||
final BftExtraData decoded =
|
final BftExtraData decoded =
|
||||||
new BftExtraData(vanityData, seals, vote, round, validators, falconSeals);
|
new BftExtraData(vanityData, seals, vote, round, validators, falconSeals, hybridSeals);
|
||||||
|
|
||||||
// AERE FIX-MALEABILITATE-BLOC (2026-08-01), gate 2 of 2: CANONICAL ROUND TRIP.
|
// AERE FIX-MALEABILITATE-BLOC (2026-08-01), gate 2 of 2: CANONICAL ROUND TRIP.
|
||||||
//
|
//
|
||||||
@ -204,7 +231,10 @@ public class QbftExtraDataCodec extends BftExtraDataCodec {
|
|||||||
// committed-seal hash and the on-chain block hash are byte-identical to upstream Besu, and a
|
// committed-seal hash and the on-chain block hash are byte-identical to upstream Besu, and a
|
||||||
// header with no certificate is byte-identical to upstream too, so the Falcon seals are
|
// header with no certificate is byte-identical to upstream too, so the Falcon seals are
|
||||||
// never part of any signed pre-image and never disturb the block hash.
|
// never part of any signed pre-image and never disturb the block hash.
|
||||||
if (!bftExtraData.getFalconSeals().isEmpty()) {
|
if (!bftExtraData.getHybridSeals().isEmpty()) {
|
||||||
|
// AERE ANCHOR V2: the scheme-tagged certificate, canonical bytes, in the same slot.
|
||||||
|
encoder.writeRaw(PqAnchorV2.encode(bftExtraData.getHybridSeals()));
|
||||||
|
} else if (!bftExtraData.getFalconSeals().isEmpty()) {
|
||||||
encoder.writeList(
|
encoder.writeList(
|
||||||
bftExtraData.getFalconSeals(),
|
bftExtraData.getFalconSeals(),
|
||||||
(falconSeal, rlp) -> {
|
(falconSeal, rlp) -> {
|
||||||
|
|||||||
@ -118,7 +118,10 @@ public class QbftBlockCreatorAdaptor implements QbftBlockCreator {
|
|||||||
initialExtraData.getVote(),
|
initialExtraData.getVote(),
|
||||||
roundNumber,
|
roundNumber,
|
||||||
initialExtraData.getValidators(),
|
initialExtraData.getValidators(),
|
||||||
initialExtraData.getFalconSeals());
|
initialExtraData.getFalconSeals(),
|
||||||
|
// AERE ANCHOR V2 (2026-09-03, D-328): the sealed block keeps the proposer's v2 certificate
|
||||||
|
// verbatim, exactly as it keeps the v1 one; dropping it here is what stalled the testnet.
|
||||||
|
initialExtraData.getHybridSeals());
|
||||||
final BlockHeader anchoredHeader =
|
final BlockHeader anchoredHeader =
|
||||||
BlockHeaderBuilder.fromHeader(AdaptorUtil.toBesuBlockHeader(initialHeader))
|
BlockHeaderBuilder.fromHeader(AdaptorUtil.toBesuBlockHeader(initialHeader))
|
||||||
.extraData(bftExtraDataCodec.encode(anchoredExtraData))
|
.extraData(bftExtraDataCodec.encode(anchoredExtraData))
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
|||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorV2;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
|
||||||
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
|
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
|
||||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||||
import org.hyperledger.besu.ethereum.mainnet.DetachedBlockHeaderValidationRule;
|
import org.hyperledger.besu.ethereum.mainnet.DetachedBlockHeaderValidationRule;
|
||||||
@ -152,6 +154,20 @@ public class PqAnchorDigestRule implements DetachedBlockHeaderValidationRule {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AERE ANCHOR V2 (2026-09-03): from the v2 height the ONLY accepted form is the scheme-tagged
|
||||||
|
// certificate under the v2 digest; below it a v2 certificate is refused. Both directions
|
||||||
|
// are checked so the switch is a clean fork and never a header two nodes read differently.
|
||||||
|
if (config.anchorV2AppliesAt(number)) {
|
||||||
|
return judgeV2(number, header, extraData, vanity);
|
||||||
|
}
|
||||||
|
if (!extraData.getHybridSeals().isEmpty()) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R1): block {} REJECTED - carries a v2 (scheme-tagged) certificate below"
|
||||||
|
+ " the v2 activation height {}.",
|
||||||
|
number,
|
||||||
|
config.anchorV2Block());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final List<FalconSeal> certificate = List.copyOf(extraData.getFalconSeals());
|
final List<FalconSeal> certificate = List.copyOf(extraData.getFalconSeals());
|
||||||
// Height aware since 2026-08-11. Inside the one measured historical window the ordering
|
// Height aware since 2026-08-11. Inside the one measured historical window the ordering
|
||||||
// requirement relaxes from SORTED to DISTINCT, because 36 canonical headers were written
|
// requirement relaxes from SORTED to DISTINCT, because 36 canonical headers were written
|
||||||
@ -203,6 +219,36 @@ public class PqAnchorDigestRule implements DetachedBlockHeaderValidationRule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** AERE ANCHOR V2: vanityData must be the v2 digest over the carried scheme-tagged certificate. */
|
||||||
|
private boolean judgeV2(
|
||||||
|
final long number, final BlockHeader header, final BftExtraData extraData, final Bytes vanity) {
|
||||||
|
final List<SchemeSeal> seals = extraData.getHybridSeals();
|
||||||
|
if (seals.isEmpty()) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R1): block {} REJECTED - at or above the v2 activation height {} the header"
|
||||||
|
+ " carries no scheme-tagged certificate ({} v1 Falcon seal(s) instead).",
|
||||||
|
number,
|
||||||
|
config.anchorV2Block(),
|
||||||
|
extraData.getFalconSeals().size());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Bytes32 expected =
|
||||||
|
PqAnchorV2.anchorDigestV2(
|
||||||
|
config.chainId(), number - 1L, header.getParentHash().getBytes(), seals);
|
||||||
|
if (!expected.equals(Bytes32.wrap(vanity))) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R1): block {} REJECTED - v2 anchor digest mismatch. vanityData carries {}"
|
||||||
|
+ " but the {}-seal scheme-tagged certificate actually carried digests to {}. The"
|
||||||
|
+ " certificate was stripped, replaced or reordered after the proposer wrote it.",
|
||||||
|
number,
|
||||||
|
vanity,
|
||||||
|
seals.size(),
|
||||||
|
expected);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept a header from a named unenforced window as it was written, after the one check that is
|
* Accept a header from a named unenforced window as it was written, after the one check that is
|
||||||
* not relaxed at any height.
|
* not relaxed at any height.
|
||||||
|
|||||||
@ -18,18 +18,28 @@ import org.hyperledger.besu.consensus.common.bft.BftContext;
|
|||||||
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
|
import org.hyperledger.besu.consensus.common.bft.FalconSealSupport;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.HybridSealSupport;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.HybridSignerRegistry;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorLapse;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorV2;
|
||||||
import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
|
import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealScheme;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
|
||||||
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
|
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
|
||||||
import org.hyperledger.besu.datatypes.Address;
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
import org.hyperledger.besu.ethereum.ProtocolContext;
|
import org.hyperledger.besu.ethereum.ProtocolContext;
|
||||||
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||||
import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule;
|
import org.hyperledger.besu.ethereum.mainnet.AttachedBlockHeaderValidationRule;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.OptionalInt;
|
import java.util.OptionalInt;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -171,6 +181,18 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
|
|||||||
observeValidatorsForDiagnostics(parent, protocolContext);
|
observeValidatorsForDiagnostics(parent, protocolContext);
|
||||||
|
|
||||||
final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
|
final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
|
||||||
|
// AERE ANCHOR V2 (2026-09-03): one form per height, both directions refused (see R1).
|
||||||
|
if (config.anchorV2AppliesAt(number)) {
|
||||||
|
return judgeV2(number, parent, extraData, protocolContext);
|
||||||
|
}
|
||||||
|
if (!extraData.getHybridSeals().isEmpty()) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - carries a v2 (scheme-tagged) certificate below"
|
||||||
|
+ " the v2 activation height {}.",
|
||||||
|
number,
|
||||||
|
config.anchorV2Block());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final List<FalconSeal> certificate = List.copyOf(extraData.getFalconSeals());
|
final List<FalconSeal> certificate = List.copyOf(extraData.getFalconSeals());
|
||||||
final int k = certificate.size();
|
final int k = certificate.size();
|
||||||
|
|
||||||
@ -244,59 +266,9 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
|
|||||||
PqAnchor.commitMessage(
|
PqAnchor.commitMessage(
|
||||||
config.chainId(), parent.getNumber(), parent.getHash().getBytes());
|
config.chainId(), parent.getNumber(), parent.getHash().getBytes());
|
||||||
|
|
||||||
final Set<Address> counted = new HashSet<>();
|
if (!falconSealsVerify(number, parent, eligible, message, certificate)) {
|
||||||
for (final FalconSeal seal : certificate) {
|
return false;
|
||||||
final int index = seal.getValidatorIndex();
|
|
||||||
// D-081: the key set is resolved AT THE PARENT'S HEIGHT, because that is the block these
|
|
||||||
// seals commit to. Resolving at this block's height would be wrong by one block, and at a
|
|
||||||
// rotation height "wrong by one block" is a different key set and a permanent chain stop.
|
|
||||||
// D2 (b-v2): the HISTORY door. This rule judges a header this node received, at the
|
|
||||||
// PARENT's height, which is the height the certificate commits to. It must refuse a
|
|
||||||
// height it has no registry binding for rather than answer from its own head.
|
|
||||||
final Address signer =
|
|
||||||
registry.addressForIndexAtHistoric(parent.getNumber(), index);
|
|
||||||
if (signer == null) {
|
|
||||||
LOG.warn(
|
|
||||||
"AERE PQ ANCHOR (R2): block {} REJECTED - certificate carries index {}, which the "
|
|
||||||
+ "registry does not bind to any validator address. An unbound index cannot be "
|
|
||||||
+ "shown to be eligible, so it is refused rather than skipped.",
|
|
||||||
number,
|
|
||||||
index);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!eligible.contains(signer)) {
|
|
||||||
LOG.warn(
|
|
||||||
"AERE PQ ANCHOR (R2): block {} REJECTED - certificate carries index {} (address {}), "
|
|
||||||
+ "which was NOT a validator of the parent block {}.",
|
|
||||||
number,
|
|
||||||
index,
|
|
||||||
signer,
|
|
||||||
parent.getNumber());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!counted.add(signer)) {
|
|
||||||
LOG.warn(
|
|
||||||
"AERE PQ ANCHOR (R2): block {} REJECTED - two distinct registry indices, including "
|
|
||||||
+ "{}, resolve to the same validator address {}. That would let one validator be "
|
|
||||||
+ "counted twice despite strictly increasing indices.",
|
|
||||||
number,
|
|
||||||
index,
|
|
||||||
signer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!registry.verifyAtHistoric(
|
|
||||||
parent.getNumber(), index, message, seal.getSignature())) {
|
|
||||||
LOG.warn(
|
|
||||||
"AERE PQ ANCHOR (R2): block {} REJECTED - the Falcon signature from index {} "
|
|
||||||
+ "(address {}) does not verify over M(parent {}).",
|
|
||||||
number,
|
|
||||||
index,
|
|
||||||
signer,
|
|
||||||
parent.getNumber());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"AERE PQ ANCHOR (R2): block {} accepted with {} verified Falcon seal(s) over parent {} "
|
"AERE PQ ANCHOR (R2): block {} accepted with {} verified Falcon seal(s) over parent {} "
|
||||||
+ "(threshold K={}, |parent validators|={}).",
|
+ "(threshold K={}, |parent validators|={}).",
|
||||||
@ -329,6 +301,211 @@ public class PqAnchorSealsRule implements AttachedBlockHeaderValidationRule {
|
|||||||
* @param number its height
|
* @param number its height
|
||||||
* @return true iff the carried certificate decodes and its indices are distinct and non-negative
|
* @return true iff the carried certificate decodes and its indices are distinct and non-negative
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* The Falcon half, shared by the v1 and the v2 form: every seal from an index the registry binds
|
||||||
|
* to a validator of the parent, no two indices on one address, each verifying over M(parent).
|
||||||
|
*/
|
||||||
|
private boolean falconSealsVerify(
|
||||||
|
final long number,
|
||||||
|
final BlockHeader parent,
|
||||||
|
final Set<Address> eligible,
|
||||||
|
final Bytes32 message,
|
||||||
|
final List<FalconSeal> certificate) {
|
||||||
|
final Set<Address> counted = new HashSet<>();
|
||||||
|
for (final FalconSeal seal : certificate) {
|
||||||
|
final int index = seal.getValidatorIndex();
|
||||||
|
// D-081: the key set is resolved AT THE PARENT'S HEIGHT, because that is the block these
|
||||||
|
// seals commit to. Resolving at this block's height would be wrong by one block, and at a
|
||||||
|
// rotation height "wrong by one block" is a different key set and a permanent chain stop.
|
||||||
|
// D2 (b-v2): the HISTORY door. This rule judges a header this node received, at the
|
||||||
|
// PARENT's height, which is the height the certificate commits to. It must refuse a
|
||||||
|
// height it has no registry binding for rather than answer from its own head.
|
||||||
|
final Address signer =
|
||||||
|
registry.addressForIndexAtHistoric(parent.getNumber(), index);
|
||||||
|
if (signer == null) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - certificate carries index {}, which the "
|
||||||
|
+ "registry does not bind to any validator address. An unbound index cannot be "
|
||||||
|
+ "shown to be eligible, so it is refused rather than skipped.",
|
||||||
|
number,
|
||||||
|
index);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!eligible.contains(signer)) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - certificate carries index {} (address {}), "
|
||||||
|
+ "which was NOT a validator of the parent block {}.",
|
||||||
|
number,
|
||||||
|
index,
|
||||||
|
signer,
|
||||||
|
parent.getNumber());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!counted.add(signer)) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - two distinct registry indices, including "
|
||||||
|
+ "{}, resolve to the same validator address {}. That would let one validator be "
|
||||||
|
+ "counted twice despite strictly increasing indices.",
|
||||||
|
number,
|
||||||
|
index,
|
||||||
|
signer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!registry.verifyAtHistoric(
|
||||||
|
parent.getNumber(), index, message, seal.getSignature())) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the Falcon signature from index {} "
|
||||||
|
+ "(address {}) does not verify over M(parent {}).",
|
||||||
|
number,
|
||||||
|
index,
|
||||||
|
signer,
|
||||||
|
parent.getNumber());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE ANCHOR V2: the scheme-tagged certificate. K Falcon seals verified exactly as in v1, and for
|
||||||
|
* EVERY extra scheme the scheme schedule names at the parent height, K seals that (a) come from an
|
||||||
|
* index also carrying a Falcon seal in this certificate and (b) verify under the hybrid registry's
|
||||||
|
* key for that index and scheme over the same M(parent). A scheme the schedule does not name is
|
||||||
|
* refused, a missing registry is refused: a node that cannot verify has not proved the header good.
|
||||||
|
*/
|
||||||
|
private boolean judgeV2(
|
||||||
|
final long number,
|
||||||
|
final BlockHeader parent,
|
||||||
|
final BftExtraData extraData,
|
||||||
|
final ProtocolContext protocolContext) {
|
||||||
|
final List<SchemeSeal> seals = extraData.getHybridSeals();
|
||||||
|
if (seals.isEmpty()) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - at or above the v2 activation height {} the header"
|
||||||
|
+ " carries no scheme-tagged certificate.",
|
||||||
|
number,
|
||||||
|
config.anchorV2Block());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final int required = config.minSealsAt(number);
|
||||||
|
final List<FalconSeal> falcon = new ArrayList<>();
|
||||||
|
final Set<Integer> falconIndices = new HashSet<>();
|
||||||
|
for (final SchemeSeal s : seals) {
|
||||||
|
if (s.getSchemeWireId() == SealSchemes.FALCON_512.wireId()) {
|
||||||
|
falcon.add(new FalconSeal(s.getValidatorIndex(), s.getSignature()));
|
||||||
|
falconIndices.add(s.getValidatorIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (falcon.size() < required) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the v2 certificate has {} Falcon seal(s), below"
|
||||||
|
+ " the threshold K={} in force at this height.",
|
||||||
|
number,
|
||||||
|
falcon.size(),
|
||||||
|
required);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final BftContext bftContext = protocolContext.getConsensusContext(BftContext.class);
|
||||||
|
final Collection<Address> parentValidators =
|
||||||
|
bftContext.getValidatorProvider().getValidatorsForBlock(parent);
|
||||||
|
if (parentValidators == null || parentValidators.isEmpty()) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the validator set of the parent block {} could"
|
||||||
|
+ " not be resolved, so seal eligibility cannot be decided.",
|
||||||
|
number,
|
||||||
|
parent.getNumber());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Set<Address> eligible = new HashSet<>(parentValidators);
|
||||||
|
final Bytes32 message =
|
||||||
|
PqAnchor.commitMessage(config.chainId(), parent.getNumber(), parent.getHash().getBytes());
|
||||||
|
if (!falcon.isEmpty() && !falconSealsVerify(number, parent, eligible, message, falcon)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final HybridSealSupport hybrid = HybridSealSupport.instance();
|
||||||
|
final Set<String> requiredSchemes = new HashSet<>();
|
||||||
|
hybrid.schedule().ifPresent(sch -> requiredSchemes.addAll(sch.schemesAt(parent.getNumber())));
|
||||||
|
requiredSchemes.remove(SealSchemes.FALCON_512.id());
|
||||||
|
final Map<String, Integer> counted = new HashMap<>();
|
||||||
|
for (final SchemeSeal s : seals) {
|
||||||
|
if (s.getSchemeWireId() == SealSchemes.FALCON_512.wireId()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Optional<SealScheme> scheme = SealSchemes.byWireId(s.getSchemeWireId());
|
||||||
|
if (scheme.isEmpty() || !requiredSchemes.contains(scheme.get().id())) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the v2 certificate carries a seal of scheme 0x{},"
|
||||||
|
+ " which the scheme schedule does not name at the parent height {} (named: {}).",
|
||||||
|
number,
|
||||||
|
Integer.toHexString(s.getSchemeWireId() & 0xff),
|
||||||
|
parent.getNumber(),
|
||||||
|
requiredSchemes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!falconIndices.contains(s.getValidatorIndex())) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - a {} seal from index {} is not bound to a Falcon"
|
||||||
|
+ " seal of the same index in this certificate.",
|
||||||
|
number,
|
||||||
|
scheme.get().id(),
|
||||||
|
s.getValidatorIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Optional<HybridSignerRegistry> registry = hybrid.registry();
|
||||||
|
if (registry.isEmpty()) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the certificate carries {} seals and this node"
|
||||||
|
+ " has NO hybrid registry loaded, so it cannot verify them. A node that cannot"
|
||||||
|
+ " verify has not proved the header good.",
|
||||||
|
number,
|
||||||
|
scheme.get().id());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Optional<byte[]> pk = registry.get().publicKey(s.getValidatorIndex(), scheme.get().id());
|
||||||
|
if (pk.isEmpty()) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the hybrid registry holds no {} key for index {}.",
|
||||||
|
number,
|
||||||
|
scheme.get().id(),
|
||||||
|
s.getValidatorIndex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!scheme.get().verifyRaw(pk.get(), message.toArray(), s.getSignature().toArray())) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the {} signature from index {} does not verify"
|
||||||
|
+ " over M(parent {}).",
|
||||||
|
number,
|
||||||
|
scheme.get().id(),
|
||||||
|
s.getValidatorIndex(),
|
||||||
|
parent.getNumber());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
counted.merge(scheme.get().id(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
for (final String schemeId : requiredSchemes) {
|
||||||
|
final int have = counted.getOrDefault(schemeId, 0);
|
||||||
|
if (have < required) {
|
||||||
|
LOG.warn(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} REJECTED - the v2 certificate has {} valid {} seal(s), below"
|
||||||
|
+ " the threshold K={} the schedule demands of every named scheme.",
|
||||||
|
number,
|
||||||
|
have,
|
||||||
|
schemeId,
|
||||||
|
required);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG.debug(
|
||||||
|
"AERE PQ ANCHOR (R2): block {} accepted with a {}-seal scheme-tagged certificate over parent"
|
||||||
|
+ " {} (K={} per scheme, schemes={}).",
|
||||||
|
number,
|
||||||
|
seals.size(),
|
||||||
|
parent.getNumber(),
|
||||||
|
required,
|
||||||
|
requiredSchemes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean acceptAsWrittenInsideLapse(final BlockHeader header, final long number) {
|
private boolean acceptAsWrittenInsideLapse(final BlockHeader header, final long number) {
|
||||||
try {
|
try {
|
||||||
final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
|
final BftExtraData extraData = extraDataCodec.decodeRaw(header.getExtraData());
|
||||||
|
|||||||
@ -0,0 +1,361 @@
|
|||||||
|
/*
|
||||||
|
* Copyright contributors to Aere Network.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
|
||||||
|
* compliance with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is
|
||||||
|
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
* implied. See the License for the specific language governing permissions and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package org.hyperledger.besu.consensus.qbft.headervalidationrules;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.CHAIN_ID;
|
||||||
|
import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.H;
|
||||||
|
import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.VALIDATORS;
|
||||||
|
import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.header;
|
||||||
|
import static org.hyperledger.besu.consensus.qbft.headervalidationrules.PqAnchorTestSupport.parentHeader;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.withSettings;
|
||||||
|
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.BftContext;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.BftExtraData;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.FalconSeal;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.HybridSealSupport;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchor;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorConfig;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqAnchorV2;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.PqSignerRegistry;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SchemeSeal;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealScheme;
|
||||||
|
import org.hyperledger.besu.consensus.common.bft.SealSchemes;
|
||||||
|
import org.hyperledger.besu.consensus.common.validator.ValidatorProvider;
|
||||||
|
import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec;
|
||||||
|
import org.hyperledger.besu.crypto.SecureRandomProvider;
|
||||||
|
import org.hyperledger.besu.datatypes.Address;
|
||||||
|
import org.hyperledger.besu.ethereum.ProtocolContext;
|
||||||
|
import org.hyperledger.besu.ethereum.core.BlockHeader;
|
||||||
|
import org.hyperledger.besu.ethereum.rlp.RLPException;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
import org.apache.tuweni.bytes.Bytes;
|
||||||
|
import org.apache.tuweni.bytes.Bytes32;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AERE ANCHOR V2 (2026-09-03): the two header rules judge the SCHEME-TAGGED certificate, and the codec
|
||||||
|
* carries it in the sixth extraData element beside the v1 form.
|
||||||
|
*
|
||||||
|
* <p>Falcon seals come from a fake registry (a seal is valid iff it is exactly what the fake signs for
|
||||||
|
* that index over M(parent)); SLH-DSA seals are REAL, from probe keys generated here and published in
|
||||||
|
* a hybrid-1 registry loaded through the real system-configuration path.
|
||||||
|
*
|
||||||
|
* <p>Proven, each with its pair: R1 accepts the honest v2 header and refuses a stripped certificate
|
||||||
|
* (digest mismatch), a v2 certificate below the v2 height, and a v1 certificate at the v2 height; R2
|
||||||
|
* accepts the honest v2 header and refuses an SLH-DSA seal from an index without a Falcon seal, a
|
||||||
|
* certificate short of one scheme, a tampered SLH-DSA signature, and a scheme the schedule does not
|
||||||
|
* name; the codec round-trips both forms and refuses a non-canonical v2 certificate.
|
||||||
|
*/
|
||||||
|
public class PqAnchorV2RulesTest {
|
||||||
|
|
||||||
|
private static final long V2 = H + 100L;
|
||||||
|
private static final int K = 3;
|
||||||
|
private static final String SLH = "slh-dsa-sha2-128s";
|
||||||
|
private static final QbftExtraDataCodec CODEC = new QbftExtraDataCodec();
|
||||||
|
|
||||||
|
@TempDir Path tmp;
|
||||||
|
|
||||||
|
private final SecureRandom random = SecureRandomProvider.createSecureRandom();
|
||||||
|
private final List<SealScheme.GeneratedPair> slhKeys = new ArrayList<>();
|
||||||
|
private final FakeFalconRegistry falcon = new FakeFalconRegistry();
|
||||||
|
|
||||||
|
private PqAnchorConfig armedV2;
|
||||||
|
private PqAnchorDigestRule r1;
|
||||||
|
private PqAnchorSealsRule r2;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws Exception {
|
||||||
|
for (int i = 0; i < VALIDATORS.size(); i++) {
|
||||||
|
slhKeys.add(SealSchemes.SLH_DSA_128S.generate(random));
|
||||||
|
}
|
||||||
|
armHybrid("0:falcon-512+" + SLH);
|
||||||
|
armedV2 = new PqAnchorConfig(
|
||||||
|
CHAIN_ID, H, Map.of(H, 0, V2, K), OptionalInt.empty(), false, OptionalInt.empty(),
|
||||||
|
OptionalInt.empty(), V2);
|
||||||
|
r1 = new PqAnchorDigestRule(armedV2);
|
||||||
|
r2 = new PqAnchorSealsRule(armedV2, falcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
System.clearProperty(HybridSealSupport.PROPERTY_SCHEDULE);
|
||||||
|
System.clearProperty(HybridSealSupport.PROPERTY_REGISTRY);
|
||||||
|
HybridSealSupport.resetForTesting();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void armHybrid(final String schedule) throws Exception {
|
||||||
|
final StringBuilder p = new StringBuilder();
|
||||||
|
p.append("formatVersion=hybrid-1\nchainId=").append(CHAIN_ID).append("\ncount=")
|
||||||
|
.append(VALIDATORS.size()).append('\n');
|
||||||
|
for (int i = 0; i < VALIDATORS.size(); i++) {
|
||||||
|
p.append(i).append(".addr=").append(VALIDATORS.get(i).toHexString()).append('\n');
|
||||||
|
p.append(i).append(".key.").append(SLH).append('=')
|
||||||
|
.append(Bytes.wrap(slhKeys.get(i).publicRegistryForm()).toHexString()).append('\n');
|
||||||
|
}
|
||||||
|
final Path reg = tmp.resolve("hybrid-" + schedule.replaceAll("[^a-z0-9]", "-") + ".properties");
|
||||||
|
Files.writeString(reg, p.toString());
|
||||||
|
System.setProperty(HybridSealSupport.PROPERTY_SCHEDULE, schedule);
|
||||||
|
System.setProperty(HybridSealSupport.PROPERTY_REGISTRY, reg.toAbsolutePath().toString());
|
||||||
|
HybridSealSupport.resetForTesting();
|
||||||
|
assertThat(HybridSealSupport.instance().registry()).isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------ certificates
|
||||||
|
|
||||||
|
/** Falcon seals for indices 0..falconCount-1 and SLH-DSA seals for 0..slhCount-1, canonical. */
|
||||||
|
private List<SchemeSeal> certificate(final BlockHeader parent, final int falconCount, final int slhCount) {
|
||||||
|
final Bytes32 m = PqAnchor.commitMessage(CHAIN_ID, parent.getNumber(), parent.getHash().getBytes());
|
||||||
|
final List<SchemeSeal> out = new ArrayList<>();
|
||||||
|
for (int i = 0; i < falconCount; i++) {
|
||||||
|
out.add(new SchemeSeal(SealSchemes.FALCON_512.wireId(), i, FakeFalconRegistry.sign(i, m)));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < slhCount; i++) {
|
||||||
|
out.add(new SchemeSeal(SealSchemes.SLH_DSA_128S.wireId(), i,
|
||||||
|
Bytes.wrap(SealSchemes.SLH_DSA_128S.sign(slhKeys.get(i).privateKey(), m.toArray()).orElseThrow())));
|
||||||
|
}
|
||||||
|
out.sort(PqAnchorV2.CANONICAL);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bytes extraDataV2(final Bytes vanity, final List<SchemeSeal> tagged) {
|
||||||
|
return CODEC.encode(new BftExtraData(
|
||||||
|
vanity, Collections.emptyList(), Optional.empty(), 0, VALIDATORS, Collections.emptyList(), tagged));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockHeader honestV2(final long number, final BlockHeader parent, final List<SchemeSeal> tagged) {
|
||||||
|
final Bytes32 d = PqAnchorV2.anchorDigestV2(CHAIN_ID, number - 1L, parent.getHash().getBytes(), tagged);
|
||||||
|
return header(number, parent.getHash(), extraDataV2(d, tagged));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------- R1
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void r1AcceptsTheHonestV2HeaderAndRefusesAStrippedCertificate() {
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
final List<SchemeSeal> tagged = certificate(parent, 5, 5);
|
||||||
|
final BlockHeader honest = honestV2(V2 + 5L, parent, tagged);
|
||||||
|
assertThat(r1.validate(honest, parent)).isTrue();
|
||||||
|
// one SLH-DSA seal stripped under the ORIGINAL digest
|
||||||
|
final List<SchemeSeal> stripped = new ArrayList<>(tagged);
|
||||||
|
stripped.removeIf(s -> s.getSchemeWireId() == SealSchemes.SLH_DSA_128S.wireId() && s.getValidatorIndex() == 4);
|
||||||
|
final BftExtraData decoded = CODEC.decodeRaw(honest.getExtraData());
|
||||||
|
final BlockHeader strippedHeader =
|
||||||
|
header(V2 + 5L, parent.getHash(), extraDataV2(decoded.getVanityData(), stripped));
|
||||||
|
assertThat(r1.validate(strippedHeader, parent)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void r1RefusesAV2CertificateBelowTheV2HeightAndAV1CertificateAtIt() {
|
||||||
|
// v2 below V2: the anchor height V2-1 (interval absent => every height from H is an anchor height)
|
||||||
|
final BlockHeader parentBelow = parentHeader(V2 - 2L);
|
||||||
|
final BlockHeader v2Early = honestV2(V2 - 1L, parentBelow, certificate(parentBelow, 5, 5));
|
||||||
|
assertThat(r1.validate(v2Early, parentBelow)).isFalse();
|
||||||
|
// v1 at V2: an honest v1 header (correct v1 digest) is still refused, the form is wrong
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
final BlockHeader v1Late = PqAnchorTestSupport.honestHeader(V2 + 5L, parent.getHash(),
|
||||||
|
List.of(PqAnchorTestSupport.seal(0), PqAnchorTestSupport.seal(1), PqAnchorTestSupport.seal(2)));
|
||||||
|
assertThat(r1.validate(v1Late, parent)).isFalse();
|
||||||
|
// control: the same v1 header is accepted by a rule whose V2 lies in the future
|
||||||
|
final PqAnchorConfig v1World = new PqAnchorConfig(
|
||||||
|
CHAIN_ID, H, Map.of(H, 0, V2, K), OptionalInt.empty(), false, OptionalInt.empty(),
|
||||||
|
OptionalInt.empty(), V2 + 1_000L);
|
||||||
|
assertThat(new PqAnchorDigestRule(v1World).validate(v1Late, parent)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------- R2
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void r2AcceptsTheHonestV2HeaderWithKSealsPerScheme() {
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
assertThat(r2.validate(honestV2(V2 + 5L, parent, certificate(parent, K, K)), parent, context())).isTrue();
|
||||||
|
// more than K of each is fine too: K is a floor
|
||||||
|
assertThat(r2.validate(honestV2(V2 + 5L, parent, certificate(parent, 5, 5)), parent, context())).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void r2RefusesAnSlhDsaSealFromAnIndexWithoutAFalconSeal() {
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
final List<SchemeSeal> tagged = certificate(parent, K, K + 1); // SLH-DSA from index K has no Falcon
|
||||||
|
assertThat(r2.validate(honestV2(V2 + 5L, parent, tagged), parent, context())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void r2RefusesACertificateShortOfOneScheme() {
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
assertThat(r2.validate(honestV2(V2 + 5L, parent, certificate(parent, 5, K - 1)), parent, context()))
|
||||||
|
.describedAs("K Falcon seals and only K-1 SLH-DSA: the schedule demands K of EVERY scheme")
|
||||||
|
.isFalse();
|
||||||
|
assertThat(r2.validate(honestV2(V2 + 5L, parent, certificate(parent, K - 1, K - 1)), parent, context()))
|
||||||
|
.describedAs("short of Falcon as well")
|
||||||
|
.isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void r2RefusesATamperedSlhDsaSignature() {
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
final List<SchemeSeal> tagged = new ArrayList<>(certificate(parent, K, K));
|
||||||
|
for (int i = 0; i < tagged.size(); i++) {
|
||||||
|
final SchemeSeal s = tagged.get(i);
|
||||||
|
if (s.getSchemeWireId() == SealSchemes.SLH_DSA_128S.wireId() && s.getValidatorIndex() == 1) {
|
||||||
|
final byte[] sig = s.getSignature().toArray();
|
||||||
|
sig[sig.length / 2] ^= 0x01;
|
||||||
|
tagged.set(i, new SchemeSeal(s.getSchemeWireId(), s.getValidatorIndex(), Bytes.wrap(sig)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(r2.validate(honestV2(V2 + 5L, parent, tagged), parent, context())).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void r2RefusesASchemeTheScheduleDoesNotName() throws Exception {
|
||||||
|
armHybrid("0:falcon-512");
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
assertThat(r2.validate(honestV2(V2 + 5L, parent, certificate(parent, K, K)), parent, context()))
|
||||||
|
.describedAs("SLH-DSA seals carried while the schedule names Falcon only")
|
||||||
|
.isFalse();
|
||||||
|
assertThat(r2.validate(honestV2(V2 + 5L, parent, certificate(parent, K, 0)), parent, context()))
|
||||||
|
.describedAs("control: a Falcon-only v2 certificate under a Falcon-only schedule")
|
||||||
|
.isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------ codec
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theCodecRoundTripsBothFormsAndRefusesANonCanonicalV2Certificate() {
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
final List<SchemeSeal> tagged = certificate(parent, 2, 2);
|
||||||
|
final Bytes v2 = extraDataV2(Bytes32.ZERO, tagged);
|
||||||
|
final BftExtraData back = CODEC.decodeRaw(v2);
|
||||||
|
assertThat(back.getHybridSeals()).isEqualTo(tagged);
|
||||||
|
assertThat(back.getFalconSeals()).isEmpty();
|
||||||
|
// the v1 form still round-trips through the same slot
|
||||||
|
final List<FalconSeal> v1 = List.of(PqAnchorTestSupport.seal(0), PqAnchorTestSupport.seal(1));
|
||||||
|
final BftExtraData backV1 = CODEC.decodeRaw(PqAnchorTestSupport.extraData(Bytes32.ZERO, v1));
|
||||||
|
assertThat(backV1.getFalconSeals()).containsExactlyElementsOf(v1);
|
||||||
|
assertThat(backV1.getHybridSeals()).isEmpty();
|
||||||
|
// a v2 certificate written out of canonical order is refused at decode, not silently reordered
|
||||||
|
final List<SchemeSeal> unsorted = new ArrayList<>(tagged);
|
||||||
|
Collections.reverse(unsorted);
|
||||||
|
// the holder accepts any list; it is the ENCODER that refuses, so no header can carry it
|
||||||
|
assertThatThrownBy(() -> CODEC.encode(new BftExtraData(
|
||||||
|
Bytes32.ZERO, Collections.emptyList(), Optional.empty(), 0, VALIDATORS,
|
||||||
|
Collections.emptyList(), unsorted)))
|
||||||
|
.isInstanceOf(RuntimeException.class);
|
||||||
|
// hand-built non-canonical bytes: the same seals, encoded by hand out of order, do not decode
|
||||||
|
final Bytes canonical = PqAnchorV2.encode(tagged);
|
||||||
|
final BftExtraData holder = new BftExtraData(
|
||||||
|
Bytes32.ZERO, Collections.emptyList(), Optional.empty(), 0, VALIDATORS, Collections.emptyList(), tagged);
|
||||||
|
final Bytes good = CODEC.encode(holder);
|
||||||
|
assertThat(good.toHexString()).contains(canonical.toUnprefixedHexString());
|
||||||
|
final Bytes swapped = Bytes.fromHexString(good.toHexString().replace(
|
||||||
|
canonical.toUnprefixedHexString(), PqAnchorV2.encode(List.of(tagged.get(0))).toUnprefixedHexString()));
|
||||||
|
assertThatThrownBy(() -> CODEC.decodeRaw(swapped)).isInstanceOf(RLPException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------- D-328: the round substitution
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void replacingTheRoundInABlockKeepsTheV2Certificate() {
|
||||||
|
// 2026-09-03, testnet 28001: every sealed v2 header lost its certificate at replaceRoundInBlock,
|
||||||
|
// because that copy went through the v1 constructor; R1 then refused block 168032 for 3 hours.
|
||||||
|
final BlockHeader parent = parentHeader(V2 + 4L);
|
||||||
|
final List<SchemeSeal> tagged = certificate(parent, K, K);
|
||||||
|
final BlockHeader honest = honestV2(V2 + 5L, parent, tagged);
|
||||||
|
final org.hyperledger.besu.ethereum.core.Block block =
|
||||||
|
new org.hyperledger.besu.ethereum.core.Block(honest, org.hyperledger.besu.ethereum.core.BlockBody.empty());
|
||||||
|
final org.hyperledger.besu.ethereum.core.Block substituted =
|
||||||
|
new org.hyperledger.besu.consensus.common.bft.BftBlockInterface(CODEC)
|
||||||
|
.replaceRoundInBlock(block, 7, org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions.forCommittedSeal(CODEC));
|
||||||
|
final BftExtraData after = CODEC.decodeRaw(substituted.getHeader().getExtraData());
|
||||||
|
assertThat(after.getRound()).isEqualTo(7);
|
||||||
|
assertThat(after.getHybridSeals()).describedAs("the v2 certificate survives the round substitution").isEqualTo(tagged);
|
||||||
|
assertThat(after.getFalconSeals()).isEmpty();
|
||||||
|
assertThat(r1.validate(substituted.getHeader(), parent)).describedAs("R1 still binds it").isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------- helpers
|
||||||
|
|
||||||
|
private static ProtocolContext context() {
|
||||||
|
return contextWith(VALIDATORS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtocolContext contextWith(final Collection<Address> vs) {
|
||||||
|
final ValidatorProvider validatorProvider =
|
||||||
|
mock(ValidatorProvider.class, withSettings().strictness(Strictness.LENIENT));
|
||||||
|
when(validatorProvider.getValidatorsForBlock(any())).thenReturn(vs);
|
||||||
|
when(validatorProvider.getValidatorsAfterBlock(any())).thenReturn(vs);
|
||||||
|
final BftContext bftContext = mock(BftContext.class, withSettings().strictness(Strictness.LENIENT));
|
||||||
|
when(bftContext.getValidatorProvider()).thenReturn(validatorProvider);
|
||||||
|
when(bftContext.as(any())).thenReturn(bftContext);
|
||||||
|
return new ProtocolContext.Builder().withConsensusContext(bftContext).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Index i is VALIDATORS.get(i); a seal is valid iff it is exactly what {@link #sign} produces. */
|
||||||
|
private static final class FakeFalconRegistry implements PqSignerRegistry {
|
||||||
|
static Bytes sign(final int index, final Bytes32 message) {
|
||||||
|
final byte[] out = new byte[655];
|
||||||
|
out[0] = (byte) index;
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
out[1 + i] = message.get(i);
|
||||||
|
}
|
||||||
|
for (int i = 33; i < out.length; i++) {
|
||||||
|
out[i] = (byte) ((i * (index + 3) + 7) & 0xFF);
|
||||||
|
}
|
||||||
|
return Bytes.wrap(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address addressForIndexAtHistoric(final long blockNumber, final int validatorIndex) {
|
||||||
|
return validatorIndex >= 0 && validatorIndex < VALIDATORS.size() ? VALIDATORS.get(validatorIndex) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Address addressForIndexAtOwnHead(final long blockNumber, final int validatorIndex) {
|
||||||
|
return addressForIndexAtHistoric(blockNumber, validatorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verifyAtHistoric(
|
||||||
|
final long blockNumber, final int validatorIndex, final Bytes message, final Bytes signature) {
|
||||||
|
return message.size() == 32 && sign(validatorIndex, Bytes32.wrap(message)).equals(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verifyAtOwnHead(
|
||||||
|
final long blockNumber, final int validatorIndex, final Bytes message, final Bytes signature) {
|
||||||
|
return verifyAtHistoric(blockNumber, validatorIndex, message, signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user