The wire copy of a sealed proposal now carries the seal, proven on a test network
The first witness run caught a real gap: the proposal was signed twice - once for the local copy, once for the wire - and only the local copy carried the proposer's Falcon seal. Locally sealed, unsealed on the wire: with enforcement armed, the fleet stopped at the enforcement height even though every node was emitting. Falcon signatures are randomised, so the fix is structural rather than cosmetic: the proposal is built once, seal included, and the transmitter sends that same object - two signings would produce two valid and different proposals from the same proposer. A new plumbing test refuses the defect's return: it fails if the transmitter's wire path ever builds its own payload again. On the test network after the fix: emission covered seven of seven proposers, each sealing exactly its first proposal past the gate; with enforcement armed and emission on, the chain crossed the enforcement height with zero refusals; with emission off it stopped exactly one block short of the height, every refusal logged; and one unarmed node could not keep the chain alive. Still armed nowhere: the properties are unset, and unset means never.
This commit is contained in:
parent
3afe906fb2
commit
a9af2de488
@ -84,14 +84,14 @@ e65ba92e288aebe768909b1fee5c0b8850be74b3658c6242e6abdb91409a9521 consensus/comm
|
|||||||
e355b147fdda48c14cb6dc09b1bc1e7a7ccae3f9b709af8f8483f896086dbe4d consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/ProposalPqAttachGateTest.java
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
a994d5638940ea90f37ed6f3ed5eebc24781707f2f4555c7a079d1251ddbf893 consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayloadPqTest.java
|
||||||
|
|||||||
@ -84,9 +84,41 @@ public class QbftMessageTransmitter {
|
|||||||
final Optional<BlockAccessList> blockAccessList,
|
final Optional<BlockAccessList> blockAccessList,
|
||||||
final List<SignedData<RoundChangePayload>> roundChanges,
|
final List<SignedData<RoundChangePayload>> roundChanges,
|
||||||
final List<SignedData<PreparePayload>> prepares) {
|
final List<SignedData<PreparePayload>> prepares) {
|
||||||
|
multicastProposal(roundIdentifier, block, blockAccessList, roundChanges, prepares,
|
||||||
|
Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multicast proposal carrying the proposer's optional post-quantum seal (AERE PQ, 2026-08-30).
|
||||||
|
*
|
||||||
|
* <p>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> blockAccessList,
|
||||||
|
final List<SignedData<RoundChangePayload>> roundChanges,
|
||||||
|
final List<SignedData<PreparePayload>> prepares,
|
||||||
|
final Optional<FalconSeal> falconSeal) {
|
||||||
try {
|
try {
|
||||||
final Proposal data =
|
final Proposal data =
|
||||||
messageFactory.createProposal(
|
falconSeal.isPresent()
|
||||||
|
? messageFactory.createProposal(
|
||||||
|
roundIdentifier, block, blockAccessList, roundChanges, prepares, falconSeal)
|
||||||
|
: messageFactory.createProposal(
|
||||||
roundIdentifier, block, blockAccessList, roundChanges, prepares);
|
roundIdentifier, block, blockAccessList, roundChanges, prepares);
|
||||||
|
|
||||||
final ProposalMessageData message = ProposalMessageData.create(data);
|
final ProposalMessageData message = ProposalMessageData.create(data);
|
||||||
|
|||||||
@ -261,12 +261,31 @@ public class QbftRound {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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<FalconSeal> wireSeal =
|
||||||
|
proposal.getSignedPayload().getPayload().getFalconSeal();
|
||||||
|
if (wireSeal.isPresent()) {
|
||||||
|
transmitter.multicastProposal(
|
||||||
|
proposal.getRoundIdentifier(),
|
||||||
|
proposal.getSignedPayload().getPayload().getProposedBlock(),
|
||||||
|
proposal.getBlockAccessList(),
|
||||||
|
roundChanges,
|
||||||
|
prepares,
|
||||||
|
wireSeal);
|
||||||
|
} else {
|
||||||
transmitter.multicastProposal(
|
transmitter.multicastProposal(
|
||||||
proposal.getRoundIdentifier(),
|
proposal.getRoundIdentifier(),
|
||||||
proposal.getSignedPayload().getPayload().getProposedBlock(),
|
proposal.getSignedPayload().getPayload().getProposedBlock(),
|
||||||
proposal.getBlockAccessList(),
|
proposal.getBlockAccessList(),
|
||||||
roundChanges,
|
roundChanges,
|
||||||
prepares);
|
prepares);
|
||||||
|
}
|
||||||
if (updateStateWithProposedBlock(proposal)) {
|
if (updateStateWithProposedBlock(proposal)) {
|
||||||
sendPrepare(block);
|
sendPrepare(block);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
*
|
||||||
|
* <p>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<FalconSeal> 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<MessageData> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user