diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt
index 1105093..83f3177 100644
--- a/anchor/MANIFEST-sha256.txt
+++ b/anchor/MANIFEST-sha256.txt
@@ -110,6 +110,7 @@ bca46ca91803c89eb18d670c740edb4e47ef0bca53a9b4b82ce0c8446e0d6c0e consensus/qbft
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
53a4e0246a55c239ed44a61271e6d38c7901e1101dfa53ebd44d7a7f5bcb74ac consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqRoundChangeEnforcementTest.java
+54c98f0bb83a267b1e45ccb71f48d7bd97fc4e8dbd260b3be1dfa2e4f77a94f8 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqTransportIndependenceTest.java
dc9f9e862a11f0135d26976176a1d1adac3e84fd5f1e06572d8727262cb3374f consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PrepareValidatorPqWiringTest.java
2c612868dfdd61d86cadc4ca9b34faacf4e43aca88878feb72ac140e1321ef5d consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/ProposalPayloadValidatorPqWiringTest.java
83dd971f66ed63103d09db5283240556bf1db1e1925c2a07808c7f59d47433ad consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/RoundChangeJustificationPqTest.java
diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqTransportIndependenceTest.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqTransportIndependenceTest.java
new file mode 100644
index 0000000..a276d30
--- /dev/null
+++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/validation/PqTransportIndependenceTest.java
@@ -0,0 +1,437 @@
+/*
+ * 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.ConsensusRoundIdentifier;
+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;
+
+/**
+ * THE ADVERSARY WHO ALREADY HAS THE VALIDATOR'S ECDSA KEY, and still cannot move the chain.
+ *
+ *
WHY THIS TEST IS THE ONE THAT MATTERS. The network transport (RLPx/ECIES) authenticates
+ * a peer by its secp256k1 node key, and {@code ValidatorPeers} routes consensus messages by the
+ * address derived from that same key. A quantum adversary who recovers a validator's ECDSA key
+ * therefore gets EVERYTHING the transport can give: it completes the handshake as that validator,
+ * is routed consensus traffic as that validator, and signs syntactically perfect QBFT messages that
+ * recover to that validator's address. Every check that predates the post-quantum layers passes for
+ * it.
+ *
+ *
The question this test answers, by measurement rather than by argument: with the
+ * post-quantum enforcement armed, does that adversary gain anything at all? It models exactly
+ * that adversary - full ECDSA compromise, no Falcon key - and requires all four hot-path messages
+ * to be refused. That is what "consensus safety does not depend on the transport" means concretely,
+ * and it is the honest form of the claim: not that the transport is post-quantum (it is not), but
+ * that breaking it does not buy a break of safety.
+ *
+ *
THE CONTROLS, without which the refusals would prove nothing:
+ *
+ *
+ * - DISARMED: the very same messages from the very same adversary are ACCEPTED when the
+ * enforcement is not armed. Without this, the refusals could come from any unrelated defect
+ * in the fixture and the test would be measuring its own mistake.
+ *
- THE HONEST VALIDATOR: a message carrying a VALID Falcon seal is accepted while armed. So
+ * the refusal is caused by the missing seal, not by arming per se - otherwise an enforcement
+ * that refuses everything would look identical to one that enforces.
+ *
- THE HARVESTED SEAL, in four flavours, because a broken transport hands the adversary the
+ * victim's entire message history to replay: a seal from another message KIND, one for
+ * another BLOCK at the same height and round, one from another HEIGHT, ROUND or CHAIN, and
+ * one belonging to ANOTHER VALIDATOR entirely, stapled onto the adversary's own message.
+ *
+ *
+ * TWO OF THOSE CONTROLS EXIST BECAUSE THE NEGATIVE CONTROL CAUGHT THEIR ABSENCE
+ * (2026-08-31, {@code PROBA-CONTROL-INDEPENDENTA-TRANSPORT.sh}). The first version of this test
+ * passed with 8 green assertions, and stayed green under two planted defects: the digest removed
+ * from the signed preimage, and the index-to-author binding removed. Both were invisible because
+ * every replay it tried crossed a domain boundary, and because its registry held a single
+ * validator - so the two attacks a broken transport most directly enables could not even be
+ * expressed. Green, running, and blind. That is the whole reason a proof has to be shown red.
+ *
+ *
WHAT THIS DOES NOT PROVE, said plainly: nothing here is about LIVENESS. An adversary
+ * with a validator's ECDSA key can still occupy the connection slot, eclipse a peer, or flood it;
+ * those are denial-of-service questions and this test says nothing about them. It also says
+ * nothing about the fleet as configured today, where the enforcement heights are unset - the
+ * property proven is the property of the ARMED configuration.
+ */
+class PqTransportIndependenceTest {
+
+ private static final long H = 1_000L;
+ private static final int ROUND = 2;
+ private static final long CHAIN_ID = 2800L;
+
+ /** The validator whose ECDSA key the adversary has stolen. */
+ private static final Address VICTIM = Address.fromHexString("0x" + "dd".repeat(20));
+
+ /**
+ * A SECOND, uncompromised validator. It exists because of what the negative control measured:
+ * with a single-validator registry, the scenario "the adversary staples someone else's seal -
+ * one it overheard on the broken wire - onto its own message" cannot even be expressed, and the
+ * planted removal of the index-to-author binding left the whole test GREEN. A test that cannot
+ * express the attack does not measure it.
+ */
+ private static final Address OTHER = Address.fromHexString("0x" + "ee".repeat(20));
+
+ private static final Hash DIGEST = Hash.hash(Bytes.of(4, 2));
+
+ /**
+ * A DIFFERENT block digest at the same height and round. Also added after the negative control:
+ * every replay the first version tried crossed a DOMAIN boundary, so nothing required the
+ * digest itself to be under the seal - and the planted removal of the digest from the preimage
+ * went unnoticed. That removal is the difference between "this validator voted" and "this
+ * validator voted FOR THIS BLOCK".
+ */
+ private static final Hash OTHER_DIGEST = Hash.hash(Bytes.of(7, 7));
+
+ private static final Hash COMMIT_DIGEST = Hash.hash(Bytes.of(4, 3));
+
+ private final SecureRandom random = SecureRandomProvider.createSecureRandom();
+
+ /** The victim's Falcon key pair - held by the HONEST node, never by the adversary. */
+ private final SealScheme.GeneratedPair victimPq = SealSchemes.FALCON_512.generate(random);
+
+ /** The second validator's Falcon key pair - likewise never in the adversary's hands. */
+ private final SealScheme.GeneratedPair otherPq = SealSchemes.FALCON_512.generate(random);
+
+ /** A registry binding index 0 to the victim and index 1 to the other validator. */
+ private final PqSignerRegistry registry =
+ new PqSignerRegistry() {
+ private final Map bindings = Map.of(0, VICTIM, 1, OTHER);
+ private final Map keys =
+ Map.of(0, victimPq.publicRegistryForm(), 1, otherPq.publicRegistryForm());
+
+ @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
+ && SealSchemes.FALCON_512.verifyRaw(pk, message.toArray(), signature.toArray());
+ }
+ };
+
+ /** A seal the HONEST victim would produce for the given message. */
+ private FalconSeal honestSeal(final Bytes32 message) {
+ final byte[] sig =
+ SealSchemes.FALCON_512.sign(victimPq.privateKey(), message.toArray()).orElseThrow();
+ return new FalconSeal(0, Bytes.wrap(sig));
+ }
+
+ /**
+ * A seal the OTHER honest validator would produce - the kind the adversary can hear on a broken
+ * wire and would like to reuse as its own.
+ */
+ private FalconSeal otherValidatorSeal(final Bytes32 message) {
+ final byte[] sig =
+ SealSchemes.FALCON_512.sign(otherPq.privateKey(), message.toArray()).orElseThrow();
+ return new FalconSeal(1, Bytes.wrap(sig));
+ }
+
+ private Bytes32 prepareMsg() {
+ return PqAnchor.prepareMessage(CHAIN_ID, H, ROUND, DIGEST.getBytes());
+ }
+
+ private Bytes32 proposalMsg() {
+ return PqAnchor.proposalMessage(CHAIN_ID, H, ROUND, DIGEST.getBytes());
+ }
+
+ /**
+ * The commit layer is the OLDEST of the four and its seal covers the commit digest DIRECTLY,
+ * without a domain-separated preimage - measured in {@code PqCommitEnforcement.falconRefusal},
+ * not assumed. It is written out here rather than papered over: a test that signed a
+ * domain-separated message for commit would be testing a layer we do not have, and would fail
+ * for a reason that has nothing to do with the adversary being modelled.
+ */
+ private Bytes32 commitMsg() {
+ return Bytes32.wrap(COMMIT_DIGEST.getBytes());
+ }
+
+ private Bytes32 roundChangeMsg() {
+ return PqAnchor.roundChangeMessage(CHAIN_ID, H, ROUND);
+ }
+
+ // The four enforcements, armed at H. The adversary's messages recover to VICTIM, because the
+ // adversary holds VICTIM's ECDSA key: the author argument below is deliberately VICTIM.
+ private PqPrepareEnforcement prepareArmed() {
+ return new PqPrepareEnforcement(H, registry, CHAIN_ID);
+ }
+
+ private PqProposalEnforcement proposalArmed() {
+ return new PqProposalEnforcement(H, registry, CHAIN_ID);
+ }
+
+ private PqCommitEnforcement commitArmed() {
+ return new PqCommitEnforcement(H, registry);
+ }
+
+ private PqRoundChangeEnforcement roundChangeArmed() {
+ return new PqRoundChangeEnforcement(H, registry, CHAIN_ID);
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // THE MEASUREMENT: full ECDSA compromise, no Falcon key. All four messages refused.
+ // -----------------------------------------------------------------------------------------
+
+ @Test
+ void aStolenECDSAKeyBuysNOPrepare() {
+ final Optional refusal =
+ prepareArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.empty());
+ assertThat(refusal)
+ .describedAs("an adversary holding the victim's ECDSA key must not be able to vote")
+ .isPresent();
+ assertThat(refusal.get()).contains("carries NO post-quantum seal");
+ }
+
+ @Test
+ void aStolenECDSAKeyBuysNOProposal() {
+ final Optional refusal =
+ proposalArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.empty());
+ assertThat(refusal).isPresent();
+ assertThat(refusal.get()).contains("carries NO post-quantum seal");
+ }
+
+ @Test
+ void aStolenECDSAKeyBuysNOCommit() {
+ final Optional refusal =
+ commitArmed().refusal(H, VICTIM, COMMIT_DIGEST, Optional.empty());
+ assertThat(refusal).isPresent();
+ }
+
+ @Test
+ void aStolenECDSAKeyBuysNORoundChange() {
+ final Optional refusal =
+ roundChangeArmed().refusal(H, ROUND, VICTIM, Optional.empty(), Optional.empty());
+ assertThat(refusal).isPresent();
+ assertThat(refusal.get()).contains("carries NO post-quantum seal");
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CONTROL 1 - DISARMED: the very same messages pass. The refusals above are caused by the
+ // arming, not by something broken in this fixture.
+ // -----------------------------------------------------------------------------------------
+
+ @Test
+ void controlDISARMEDtheSameMessagesArePerfectlyValid() {
+ final long never = Long.MAX_VALUE;
+ assertThat(
+ new PqPrepareEnforcement(never, registry, CHAIN_ID)
+ .refusal(H, ROUND, VICTIM, DIGEST, Optional.empty()))
+ .isEmpty();
+ assertThat(
+ new PqProposalEnforcement(never, registry, CHAIN_ID)
+ .refusal(H, ROUND, VICTIM, DIGEST, Optional.empty()))
+ .isEmpty();
+ assertThat(
+ new PqCommitEnforcement(never, registry)
+ .refusal(H, VICTIM, COMMIT_DIGEST, Optional.empty()))
+ .isEmpty();
+ assertThat(
+ new PqRoundChangeEnforcement(never, registry, CHAIN_ID)
+ .refusal(H, ROUND, VICTIM, Optional.empty(), Optional.empty()))
+ .isEmpty();
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CONTROL 2 - THE HONEST VALIDATOR still works while armed. Otherwise "refuses everything"
+ // would be indistinguishable from "enforces".
+ // -----------------------------------------------------------------------------------------
+
+ @Test
+ void controlTheHONESTvalidatorPassesOnAllFourMessagesWhileArmed() {
+ assertThat(
+ prepareArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(honestSeal(prepareMsg()))))
+ .isEmpty();
+ assertThat(
+ proposalArmed()
+ .refusal(H, ROUND, VICTIM, DIGEST, Optional.of(honestSeal(proposalMsg()))))
+ .isEmpty();
+ assertThat(commitArmed().refusal(H, VICTIM, COMMIT_DIGEST, Optional.of(honestSeal(commitMsg()))))
+ .isEmpty();
+ assertThat(
+ roundChangeArmed()
+ .refusal(
+ H, ROUND, VICTIM, Optional.empty(), Optional.of(honestSeal(roundChangeMsg()))))
+ .isEmpty();
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CONTROL 3 - THE STOLEN SEAL. A broken transport lets the adversary READ every message the
+ // victim ever sent, so it can replay any seal it has seen. Each seal is bound to its own
+ // message, so a seal harvested from one message must not authenticate another. This is the
+ // control that turns "the adversary has no key" into "eavesdropping does not substitute for
+ // one" - and it is checked ACROSS message kinds (domain separation) and WITHIN a kind
+ // (a round-change seal replayed onto a claim of a prepared block).
+ // -----------------------------------------------------------------------------------------
+
+ @Test
+ void controlASealHARVESTEDfromAnotherMessageDoesNotAuthenticateThisOne() {
+ // a PREPARE seal, replayed on a PROPOSAL and on a ROUND-CHANGE
+ final FalconSeal harvestedPrepare = honestSeal(prepareMsg());
+ assertThat(proposalArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(harvestedPrepare)))
+ .isPresent();
+ assertThat(
+ roundChangeArmed()
+ .refusal(H, ROUND, VICTIM, Optional.empty(), Optional.of(harvestedPrepare)))
+ .isPresent();
+
+ // a PROPOSAL seal, replayed as a vote
+ final FalconSeal harvestedProposal = honestSeal(proposalMsg());
+ assertThat(prepareArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(harvestedProposal)))
+ .isPresent();
+
+ // a bare ROUND-CHANGE seal, replayed on one that claims a prepared block: same kind, different
+ // assertion, and the metadata is in the preimage precisely so this fails
+ final FalconSeal harvestedRoundChange = honestSeal(roundChangeMsg());
+ assertThat(
+ roundChangeArmed()
+ .refusal(
+ H,
+ ROUND,
+ VICTIM,
+ Optional.of(new PreparedRoundMetadata(DIGEST, 1)),
+ Optional.of(harvestedRoundChange)))
+ .isPresent();
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CONTROL 3b - THE SEAL OF ANOTHER BLOCK, same kind, same height, same round. This is the
+ // difference between "this validator voted" and "this validator voted FOR THIS BLOCK", and it
+ // is the one an eavesdropper is best placed to exploit: on a broken wire it hears the victim's
+ // honest vote for block A and wants to turn it into a vote for block B.
+ //
+ // ADDED after the negative control caught its absence: every replay above crosses a DOMAIN
+ // boundary, so with the digest planted OUT of the signed message the whole test stayed GREEN.
+ // -----------------------------------------------------------------------------------------
+
+ @Test
+ void controlASealForANOTHERBLOCKDoesNotAuthenticateThisOne() {
+ final FalconSeal forOtherBlock =
+ honestSeal(PqAnchor.prepareMessage(CHAIN_ID, H, ROUND, OTHER_DIGEST.getBytes()));
+ assertThat(prepareArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(forOtherBlock)))
+ .describedAs("a vote for one block must not authenticate a vote for another")
+ .isPresent();
+
+ final FalconSeal proposalForOtherBlock =
+ honestSeal(PqAnchor.proposalMessage(CHAIN_ID, H, ROUND, OTHER_DIGEST.getBytes()));
+ assertThat(proposalArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(proposalForOtherBlock)))
+ .isPresent();
+
+ // and the commit layer, whose seal covers the commit digest directly
+ final FalconSeal commitForOtherBlock = honestSeal(Bytes32.wrap(OTHER_DIGEST.getBytes()));
+ assertThat(commitArmed().refusal(H, VICTIM, COMMIT_DIGEST, Optional.of(commitForOtherBlock)))
+ .isPresent();
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CONTROL 3c - SOMEONE ELSE'S SEAL, stapled on. The adversary hears validator 1's perfectly
+ // valid seal for the very message it wants to send, and attaches it to its own message - which
+ // recovers to the VICTIM, because that is the ECDSA key it stole. The seal is valid, the
+ // signature is valid, the author is a real validator: only the index-to-author binding stands
+ // between that and a counted vote.
+ //
+ // ADDED after the negative control: with a single-validator registry this attack could not even
+ // be written, and the planted removal of that binding left the test GREEN.
+ // -----------------------------------------------------------------------------------------
+
+ @Test
+ void controlSOMEONEELSESsealDoesNotAuthenticateTheAdversarysMessage() {
+ final FalconSeal theirs = otherValidatorSeal(prepareMsg());
+
+ // sanity: that seal really is valid FOR ITS OWN AUTHOR, or the refusal below would prove
+ // nothing about the binding and everything about a broken fixture
+ assertThat(prepareArmed().refusal(H, ROUND, OTHER, DIGEST, Optional.of(theirs)))
+ .describedAs("validator 1's own seal must work for validator 1")
+ .isEmpty();
+
+ // the attack: the same valid seal, on a message authored with the victim's stolen key
+ assertThat(prepareArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(theirs)))
+ .describedAs("a seal must not vouch for a message someone else authored")
+ .isPresent();
+
+ // the same, on the other three surfaces
+ assertThat(
+ proposalArmed()
+ .refusal(H, ROUND, VICTIM, DIGEST, Optional.of(otherValidatorSeal(proposalMsg()))))
+ .isPresent();
+ assertThat(
+ commitArmed()
+ .refusal(H, VICTIM, COMMIT_DIGEST, Optional.of(otherValidatorSeal(commitMsg()))))
+ .isPresent();
+ assertThat(
+ roundChangeArmed()
+ .refusal(
+ H,
+ ROUND,
+ VICTIM,
+ Optional.empty(),
+ Optional.of(otherValidatorSeal(roundChangeMsg()))))
+ .isPresent();
+ }
+
+ // -----------------------------------------------------------------------------------------
+ // CONTROL 4 - A SEAL FROM ANOTHER HEIGHT OR ROUND. The transport gives the adversary the
+ // victim's whole message history, so the seals it can replay are not only from other kinds but
+ // from other positions in the chain.
+ // -----------------------------------------------------------------------------------------
+
+ @Test
+ void controlASealFromAnotherHEIGHTorROUNDDoesNotAuthenticateThisOne() {
+ final FalconSeal otherHeight =
+ honestSeal(PqAnchor.prepareMessage(CHAIN_ID, H + 1, ROUND, DIGEST.getBytes()));
+ assertThat(prepareArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(otherHeight)))
+ .isPresent();
+
+ final FalconSeal otherRound =
+ honestSeal(PqAnchor.prepareMessage(CHAIN_ID, H, ROUND + 1, DIGEST.getBytes()));
+ assertThat(prepareArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(otherRound)))
+ .isPresent();
+
+ final FalconSeal otherChain =
+ honestSeal(PqAnchor.prepareMessage(CHAIN_ID + 1, H, ROUND, DIGEST.getBytes()));
+ assertThat(prepareArmed().refusal(H, ROUND, VICTIM, DIGEST, Optional.of(otherChain)))
+ .isPresent();
+ }
+}