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