From 2ac6ecc3169df4b622e467d37e502f9cecf3b03b Mon Sep 17 00:00:00 2001 From: Aere Network Date: Tue, 1 Sep 2026 00:28:13 +0300 Subject: [PATCH] Prove scheme agility of the proposal and round-change enforcements One shared harness for both surfaces (the twin-class lesson applied in advance): a seal made with SLH-DSA passes through the same untouched consensus code, and a Falcon seal against an SLH-DSA registry is refused on both surfaces. The Falcon nail sits only in the production wiring, for all three message enforcements. --- anchor/MANIFEST-sha256.txt | 1 + .../core/validation/PqMessageAgilityTest.java | 198 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt index 6f5d727..1105093 100644 --- a/anchor/MANIFEST-sha256.txt +++ b/anchor/MANIFEST-sha256.txt @@ -105,6 +105,7 @@ df5d7d2119cf0864a8c2dce50f4f69bd8b64804cfda7f7366c013b304af0a6e7 consensus/qbft 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 5e0bb0ecc77ffb06f232e1aa81cca6870e5455af7c2846ae54d5f477f1bbb88b 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 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 afbd5e820e5dabfbaa924be2730019cc133fcbd03095f9a8a4b783d8b5e79c90 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcementTest.java diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java new file mode 100644 index 0000000..cb51fed --- /dev/null +++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqMessageAgilityTest.java @@ -0,0 +1,198 @@ +/* + * Copyright contributors to 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.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.PqAnchor; +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.qbft.core.payload.PreparedRoundMetadata; +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.Map; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; + +/** + * CRYPTOGRAPHIC AGILITY of the PROPOSAL and ROUND-CHANGE enforcements: the same consensus code, + * UNTOUCHED, accepts a seal made with a DIFFERENT post-quantum scheme. + * + *

The question and the method are {@link PqPrepareAgilityTest}'s, asked of the two surfaces + * built after it (PROPOSAL on 2026-08-30, ROUND-CHANGE on 2026-08-31): both enforcements receive + * the registry through their constructor and name no scheme, so the Falcon nail must sit only in + * the production wiring ({@code PqSignerRegistry.falconSealSupport()}), never in the enforcement + * class. If any of these tests fails, a change of maths would mean opening consensus code rather + * than adding a binding - a far more expensive finding than the test. + * + *

ONE class for both surfaces on purpose: the twin-class finding (D-293, consolidated + * 2026-08-31) measured what per-surface copies of the same idea cost - one gets a repair, the + * other silently does not. The shared harness here is the fix applied in advance. + * + *

WHAT THIS DOES NOT PROVE: that the fleet can run this way today. The production wiring stays + * Falcon-only, recorded as such in the findings register (D-285). + */ +class PqMessageAgilityTest { + + private static final long H_ARMARE = 1_000L; + private static final int ROUND = 2; + private static final long CHAIN_ID = 2800L; + private static final Address VALIDATOR_0 = Address.fromHexString("0x" + "cc".repeat(20)); + private static final Hash DIGEST = Hash.hash(Bytes.of(9, 9, 9)); + + private final SecureRandom random = SecureRandomProvider.createSecureRandom(); + + /** A registry that verifies under A GIVEN SCHEME, whichever it is. Nothing Falcon inside. */ + private static final class RegistryPerScheme implements PqSignerRegistry { + private final SealScheme scheme; + private final Map bindings; + private final Map keys; + + RegistryPerScheme( + final SealScheme scheme, final Map bindings, final Map keys) { + this.scheme = scheme; + this.bindings = bindings; + this.keys = keys; + } + + @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 = keys.get(validatorIndex); + return pk != null && scheme.verifyRaw(pk, message.toArray(), signature.toArray()); + } + } + + private RegistryPerScheme registryFor(final SealScheme scheme, final SealScheme.GeneratedPair k) { + return new RegistryPerScheme(scheme, Map.of(0, VALIDATOR_0), Map.of(0, k.publicRegistryForm())); + } + + /** A PROPOSAL signed with the given scheme, enforced over a registry on that same scheme. */ + private boolean proposalPassesUnder(final SealScheme scheme) { + final SealScheme.GeneratedPair k = scheme.generate(random); + final Bytes32 message = PqAnchor.proposalMessage(CHAIN_ID, H_ARMARE, ROUND, DIGEST.getBytes()); + final byte[] sig = scheme.sign(k.privateKey(), message.toArray()).orElseThrow(); + final PqProposalEnforcement enforcement = + new PqProposalEnforcement(H_ARMARE, registryFor(scheme, k), CHAIN_ID); + return enforcement + .refusal(H_ARMARE, ROUND, VALIDATOR_0, DIGEST, Optional.of(new FalconSeal(0, Bytes.wrap(sig)))) + .isEmpty(); + } + + /** + * A ROUND-CHANGE signed with the given scheme, enforced over a registry on that same scheme - + * bare or claiming a prepared block, because the two preimages differ and both must stay + * scheme-free. + */ + private boolean roundChangePassesUnder(final SealScheme scheme, final boolean withPrepared) { + final SealScheme.GeneratedPair k = scheme.generate(random); + final Bytes32 message = + withPrepared + ? PqAnchor.roundChangeMessage(CHAIN_ID, H_ARMARE, ROUND, 1, DIGEST.getBytes()) + : PqAnchor.roundChangeMessage(CHAIN_ID, H_ARMARE, ROUND); + final byte[] sig = scheme.sign(k.privateKey(), message.toArray()).orElseThrow(); + final PqRoundChangeEnforcement enforcement = + new PqRoundChangeEnforcement(H_ARMARE, registryFor(scheme, k), CHAIN_ID); + final Optional prm = + withPrepared ? Optional.of(new PreparedRoundMetadata(DIGEST, 1)) : Optional.empty(); + return enforcement + .refusal(H_ARMARE, ROUND, VALIDATOR_0, prm, Optional.of(new FalconSeal(0, Bytes.wrap(sig)))) + .isEmpty(); + } + + @Test + void aProposalSignedWithFALCONPasses() { + // THE WITNESS. Without it, a "passes" for SLH-DSA would not say whether the enforcement + // verifies anything at all. + assertThat(proposalPassesUnder(SealSchemes.FALCON_512)).isTrue(); + } + + @Test + void aProposalSignedWithSLHDSAPassesTHESAMEWay() { + // The same enforcement class, the same message, THE SAME consensus code - different maths. + assertThat(proposalPassesUnder(SealSchemes.SLH_DSA_128S)).isTrue(); + } + + @Test + void aRoundChangeSignedWithFALCONPasses() { + assertThat(roundChangePassesUnder(SealSchemes.FALCON_512, false)).isTrue(); + assertThat(roundChangePassesUnder(SealSchemes.FALCON_512, true)).isTrue(); + } + + @Test + void aRoundChangeSignedWithSLHDSAPassesTHESAMEWay() { + assertThat(roundChangePassesUnder(SealSchemes.SLH_DSA_128S, false)).isTrue(); + assertThat(roundChangePassesUnder(SealSchemes.SLH_DSA_128S, true)).isTrue(); + } + + @Test + void neitherEnforcementNAMESASchemeAtAll() { + // The control that makes "passes" mean something: a Falcon seal against an SLH-DSA registry + // must be REFUSED on both surfaces. Otherwise "passes" could just mean "does not verify". + final SealScheme.GeneratedPair falcon = SealSchemes.FALCON_512.generate(random); + final SealScheme.GeneratedPair slh = SealSchemes.SLH_DSA_128S.generate(random); + final RegistryPerScheme slhRegistry = registryFor(SealSchemes.SLH_DSA_128S, slh); + + final Bytes32 propMsg = PqAnchor.proposalMessage(CHAIN_ID, H_ARMARE, ROUND, DIGEST.getBytes()); + final byte[] propSigFalcon = + SealSchemes.FALCON_512.sign(falcon.privateKey(), propMsg.toArray()).orElseThrow(); + assertThat( + new PqProposalEnforcement(H_ARMARE, slhRegistry, CHAIN_ID) + .refusal( + H_ARMARE, + ROUND, + VALIDATOR_0, + DIGEST, + Optional.of(new FalconSeal(0, Bytes.wrap(propSigFalcon))))) + .isPresent(); + + final Bytes32 rcMsg = PqAnchor.roundChangeMessage(CHAIN_ID, H_ARMARE, ROUND); + final byte[] rcSigFalcon = + SealSchemes.FALCON_512.sign(falcon.privateKey(), rcMsg.toArray()).orElseThrow(); + assertThat( + new PqRoundChangeEnforcement(H_ARMARE, slhRegistry, CHAIN_ID) + .refusal( + H_ARMARE, + ROUND, + VALIDATOR_0, + Optional.empty(), + Optional.of(new FalconSeal(0, Bytes.wrap(rcSigFalcon))))) + .isPresent(); + } +}