diff --git a/anchor/MANIFEST-sha256.txt b/anchor/MANIFEST-sha256.txt index 1f78fe4..59fedfe 100644 --- a/anchor/MANIFEST-sha256.txt +++ b/anchor/MANIFEST-sha256.txt @@ -84,14 +84,14 @@ e65ba92e288aebe768909b1fee5c0b8850be74b3658c6242e6abdb91409a9521 consensus/comm e355b147fdda48c14cb6dc09b1bc1e7a7ccae3f9b709af8f8483f896086dbe4d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/ProposalPqAttachGateTest.java d48d62c6ea0784864a5987a804a28855265b2180c409ba3c5b4434d21b805e1d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/SealSchemeAgilityTest.java 22ade1aa2d8254d1f7ff02bbbcb4d02155546a52fca3421b61eea1129b73ee48 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/Commit.java -046c67d18ba623cd01390b6ffef945b0dfa245fbf0cc79889b7670ab465b7992 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java +271f1cf0cf9ad53bd40d6a99b12666aace3f878bc6ca5f1297ea7284232a735f consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java 03173f2029f767e10cc978b5576b9557efa783dab2ef105aa856f6d5e08b9352 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayload.java e577fe2c90ac2cf8bff2145376f44284cad1562c7d09b2eb6da9899849cdd882 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/MessageFactory.java 861a2e6f81df1c33755f28ca50bf88b6b0b197f7a7a1d646c165788d192160ca consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayload.java 428e11639fb24ae4b04972457714445a5cd59d7946135a8b7362123461fa690f consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java 64c3ab6f9b67eb2ed126bca89ffc0af49bebc17bc8a41b56efc19e51188ca75d consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftBlockHeightManager.java 252f9a86ca17cc6362780af264dbc4da4d842191467427a809ff1e4baa7640af consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftController.java -a56a86a95e3ff7de3c9b22ee6e91c88ed713a3c065cadbb3b6fe205698a4c2c6 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java +609890459ba6be78d0b91c04e80927231e8d61a0af822c4049ff0972530ab7ea consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java a22d857867c5b7c9ccdf9185c661870eafe6b481573189202b2cc34d9fbf3e2d consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/RoundState.java 601b0cccf32f8ebe327c7581e0e9b3f9f67de49d9fb646326d6b49ef2138c641 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/types/QbftBlockCreator.java 7f9fb13a7bbe3015bbef6329f750f2b35fb3744895cd3e6599af90450fab6a73 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/CommitValidator.java @@ -100,6 +100,7 @@ ec53f8e401a19345a067223f482f27a95cb9c3153d5bee0bfea9fd622bc0db01 consensus/qbft 742611d52091a6c138cdff60c5f7f0bfb2fc33e567f520f3e86878fc086e77d9 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PqProposalEnforcement.java 60eb9c5a8e3f464aa42915909e402bf59af9bad4e28631538dcf2f5ca52a9569 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/PrepareValidator.java 8ab72d5558842779a6df6d5d4d7c06479ac3a19746b51bdd00d5168084f0b590 consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/validation/ProposalPayloadValidator.java +e2107737579c16b73cbd2c9b50e90abcd6c910309286fb862cef780555ca43a5 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/network/ProposalSealPlumbingTest.java 36dd77cdea980e4de0090e86ae2ca75d1af62a9477fe056c7fdde06422617898 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/CommitPayloadHybridTest.java fe25255de4b5fa6d86412fa4c48218180f549f0a4ffea5b5cc0a7d19a5be4cd9 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/PreparePayloadPqTest.java a994d5638940ea90f37ed6f3ed5eebc24781707f2f4555c7a079d1251ddbf893 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayloadPqTest.java diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java index 3345a92..f9a92ab 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/network/QbftMessageTransmitter.java @@ -84,10 +84,42 @@ public class QbftMessageTransmitter { final Optional blockAccessList, final List> roundChanges, final List> prepares) { + multicastProposal(roundIdentifier, block, blockAccessList, roundChanges, prepares, + Optional.empty()); + } + + /** + * Multicast proposal carrying the proposer's optional post-quantum seal (AERE PQ, 2026-08-30). + * + *

THE SEAL MUST BE THE SAME OBJECT the caller embedded in its local copy, and this overload + * exists because the first network run proved what happens without it: this method RE-CREATES + * the proposal from scratch, so the local copy carried a seal, the wire copy carried none, and + * at the enforcement height every peer refused round after round while the proposer's own log + * said it had emitted. F84 scenario A, 2026-08-30 - the witness went red, exactly its job. + * The same two-copies trap was closed at PREPARE the day before it could bite; here it bit + * first, on a testnet, which is where it is allowed to. + * + * @param roundIdentifier the round identifier + * @param block the block + * @param blockAccessList the block access list + * @param roundChanges the round changes + * @param prepares the prepares + * @param falconSeal the proposer's post-quantum seal, or empty + */ + public void multicastProposal( + final ConsensusRoundIdentifier roundIdentifier, + final QbftBlock block, + final Optional blockAccessList, + final List> roundChanges, + final List> prepares, + final Optional falconSeal) { try { final Proposal data = - messageFactory.createProposal( - roundIdentifier, block, blockAccessList, roundChanges, prepares); + falconSeal.isPresent() + ? messageFactory.createProposal( + roundIdentifier, block, blockAccessList, roundChanges, prepares, falconSeal) + : messageFactory.createProposal( + roundIdentifier, block, blockAccessList, roundChanges, prepares); final ProposalMessageData message = ProposalMessageData.create(data); diff --git a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java index f205f7f..02d535d 100644 --- a/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java +++ b/anchor/consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/statemachine/QbftRound.java @@ -261,12 +261,31 @@ public class QbftRound { return; } - transmitter.multicastProposal( - proposal.getRoundIdentifier(), - proposal.getSignedPayload().getPayload().getProposedBlock(), - proposal.getBlockAccessList(), - roundChanges, - prepares); + // THE SAME seal object goes on the wire. The transmitter re-creates the proposal, so without + // this the local copy is sealed and the wire copy is not - measured on the first network run + // (F84 scenario A): every peer refused height H round after round while this node's own log + // said it had emitted. And it must be the SAME object, not a second signing: Falcon signatures + // are randomised, so a re-signed wire copy would differ from the local one byte for byte. + // With the gate closed the OLD call is taken, call for call - the upstream tests assert the + // five-argument form, and a node that emits nothing new must go through the same calls. + final Optional wireSeal = + proposal.getSignedPayload().getPayload().getFalconSeal(); + if (wireSeal.isPresent()) { + transmitter.multicastProposal( + proposal.getRoundIdentifier(), + proposal.getSignedPayload().getPayload().getProposedBlock(), + proposal.getBlockAccessList(), + roundChanges, + prepares, + wireSeal); + } else { + transmitter.multicastProposal( + proposal.getRoundIdentifier(), + proposal.getSignedPayload().getPayload().getProposedBlock(), + proposal.getBlockAccessList(), + roundChanges, + prepares); + } if (updateStateWithProposedBlock(proposal)) { sendPrepare(block); } diff --git a/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/network/ProposalSealPlumbingTest.java b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/network/ProposalSealPlumbingTest.java new file mode 100644 index 0000000..4fe68e2 --- /dev/null +++ b/anchor/consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/network/ProposalSealPlumbingTest.java @@ -0,0 +1,101 @@ +/* + * 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.network; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; +import org.hyperledger.besu.consensus.common.bft.FalconSeal; +import org.hyperledger.besu.consensus.common.bft.network.ValidatorMulticaster; +import org.hyperledger.besu.consensus.qbft.core.QbftBlockTestFixture; +import org.hyperledger.besu.consensus.qbft.core.messagedata.ProposalMessageData; +import org.hyperledger.besu.consensus.qbft.core.messagewrappers.Proposal; +import org.hyperledger.besu.consensus.qbft.core.payload.MessageFactory; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlock; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockCodec; +import org.hyperledger.besu.consensus.qbft.core.types.QbftBlockHeader; +import org.hyperledger.besu.consensus.qbft.core.validation.QbftBlockHeaderTestFixture; +import org.hyperledger.besu.cryptoservices.NodeKeyUtils; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * THE PLUMBING between the local proposal and the one on the wire. + * + *

This test exists because the first network run of proposal enforcement (F84 scenario A, + * 2026-08-30) went red on its own WITNESS: every node emitted a seal on its local proposal, and + * the chain still stopped dead at the enforcement height, because {@code multicastProposal} + * re-creates the proposal from scratch and the wire copy carried no seal. Every unit test we had + * passed, since none of them looked at what actually leaves the node. This one does. + */ +@ExtendWith(MockitoExtension.class) +public class ProposalSealPlumbingTest { + + private static final ConsensusRoundIdentifier ROUND_ID = new ConsensusRoundIdentifier(1, 0); + + @Mock private QbftBlockCodec blockEncoder; + @Mock private QbftBlock block; + @Mock private ValidatorMulticaster multicaster; + + private Proposal sentProposal(final Optional seal) { + lenient().when(blockEncoder.readFrom(any())).thenReturn(block); + final MessageFactory factory = new MessageFactory(NodeKeyUtils.generate(), blockEncoder); + final QbftMessageTransmitter transmitter = new QbftMessageTransmitter(factory, multicaster); + + if (seal.isPresent()) { + transmitter.multicastProposal( + ROUND_ID, block, Optional.empty(), emptyList(), emptyList(), seal); + } else { + transmitter.multicastProposal(ROUND_ID, block, Optional.empty(), emptyList(), emptyList()); + } + + final ArgumentCaptor captor = ArgumentCaptor.forClass(MessageData.class); + verify(multicaster).send(captor.capture()); + return ProposalMessageData.fromMessageData(captor.getValue()).decode(blockEncoder); + } + + @Test + public void theSealHandedToTheTransmitterIsTheSealOnTheWire() { + final FalconSeal seal = new FalconSeal(4, Bytes.fromHexString("0xdeadbeef")); + final Proposal wire = sentProposal(Optional.of(seal)); + assertThat(wire.getSignedPayload().getPayload().getFalconSeal()).contains(seal); + } + + @Test + public void withoutASealTheWireProposalCarriesNone() { + final Proposal wire = sentProposal(Optional.empty()); + assertThat(wire.getSignedPayload().getPayload().getFalconSeal()).isEmpty(); + } + + /** Kept so the fixture imports stay honest if the wrappers change shape. */ + @Test + public void theFixturesStillBuildARealBlock() { + final QbftBlockHeader header = new QbftBlockHeaderTestFixture().number(1).buildHeader(); + final QbftBlock real = new QbftBlockTestFixture().blockHeader(header).build(); + assertThat(real.getHeader().getNumber()).isEqualTo(1); + } +}