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.
This commit is contained in:
Aere Network 2026-09-01 00:28:13 +03:00
parent 473832438c
commit 2ac6ecc316
2 changed files with 199 additions and 0 deletions

View File

@ -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

View File

@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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<Integer, Address> bindings;
private final Map<Integer, byte[]> keys;
RegistryPerScheme(
final SealScheme scheme, final Map<Integer, Address> bindings, final Map<Integer, byte[]> 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<PreparedRoundMetadata> 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();
}
}