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:
parent
473832438c
commit
2ac6ecc316
@ -105,6 +105,7 @@ df5d7d2119cf0864a8c2dce50f4f69bd8b64804cfda7f7366c013b304af0a6e7 consensus/qbft
|
|||||||
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
|
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
|
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
|
||||||
afbd5e820e5dabfbaa924be2730019cc133fcbd03095f9a8a4b783d8b5e79c90 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcementTest.java
|
afbd5e820e5dabfbaa924be2730019cc133fcbd03095f9a8a4b783d8b5e79c90 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcementTest.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.
|
||||||
|
*
|
||||||
|
* <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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user