Skip to content

Commit 4721226

Browse files
authored
Fix QBFT ProposalPayload backward compatibility with pre-26.1.0 Besu versions (#9977)
Signed-off-by: Jason Frame <jason.frame@consensys.net>
1 parent 3c40b71 commit 4721226

File tree

3 files changed

+55
-0
lines changed

3 files changed

+55
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
### Bug fixes
1717
- BFT forks that change block period on time-based forks don't take effect [9681](https://github.com/hyperledger/besu/issues/9681)
18+
- Fix QBFT `RLPException` when decoding proposals from pre-26.1.0 nodes that do not include the `blockAccessList` field [#9977](https://github.com/hyperledger/besu/pull/9977)
1819

1920
### Additions and Improvements
2021
- Add IPv6 dual-stack support for DiscV5 peer discovery (enabled via `--Xv5-discovery-enabled`): new `--p2p-host-ipv6`, `--p2p-interface-ipv6`, and `--p2p-port-ipv6` CLI options enable a second UDP discovery socket; `--p2p-ipv6-outbound-enabled` controls whether IPv6 is preferred for outbound connections when a peer advertises both address families [#9763](https://github.com/hyperledger/besu/pull/9763); RLPx now also binds a second TCP socket on the IPv6 interface so IPv6-only peers can establish connections [#9873](https://github.com/hyperledger/besu/pull/9873)

consensus/qbft-core/src/main/java/org/hyperledger/besu/consensus/qbft/core/payload/ProposalPayload.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ public String toString() {
154154
}
155155

156156
private static Optional<BlockAccessList> readBlockAccessList(final RLPInput rlpInput) {
157+
if (rlpInput.isEndOfCurrentList()) {
158+
// Backward compatibility: pre-26.1.0 messages do not include blockAccessList
159+
return Optional.empty();
160+
}
157161
if (!rlpInput.nextIsNull()) {
158162
return Optional.of(BlockAccessListDecoder.decode(rlpInput));
159163
}

consensus/qbft-core/src/test/java/org/hyperledger/besu/consensus/qbft/core/messagewrappers/ProposalTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.hyperledger.besu.datatypes.Address;
3434
import org.hyperledger.besu.ethereum.core.Util;
3535
import org.hyperledger.besu.ethereum.mainnet.block.access.list.BlockAccessList;
36+
import org.hyperledger.besu.ethereum.rlp.RLPInput;
37+
import org.hyperledger.besu.ethereum.rlp.RLPOutput;
3638

3739
import java.util.List;
3840
import java.util.Optional;
@@ -104,6 +106,54 @@ private SignedData<RoundChangePayload> createRoundChange(final NodeKey nodeKey)
104106
nodeKey.sign(Bytes32.wrap(roundChangePayload.hashForSignature().getBytes())));
105107
}
106108

109+
@Test
110+
public void canDecodeProposalMessageFromLegacyNodeWithoutBlockAccessList() {
111+
// Simulate a pre-26.1.0 validator that encodes ProposalPayload WITHOUT the blockAccessList
112+
// field.
113+
// Old format payload list: [seqNum, roundNum, block] (3 items)
114+
// New format payload list: [seqNum, roundNum, block, null] (4 items)
115+
116+
// The mock must consume the block bytes written by the legacy writeTo override, otherwise the
117+
// RLP cursor remains on the block hash and readBlockAccessList sees it instead of end-of-list.
118+
when(blockEncoder.readFrom(any()))
119+
.thenAnswer(
120+
inv -> {
121+
inv.<RLPInput>getArgument(0).skipNext();
122+
return BLOCK;
123+
});
124+
125+
final NodeKey nodeKey = NodeKeyUtils.generate();
126+
final ConsensusRoundIdentifier roundIdentifier = new ConsensusRoundIdentifier(1, 1);
127+
128+
// ProposalPayload subclass that overrides writeTo() to omit the blockAccessList field,
129+
// reproducing the pre-26.1.0 wire format
130+
final ProposalPayload oldFormatPayload =
131+
new ProposalPayload(roundIdentifier, BLOCK, blockEncoder) {
132+
@Override
133+
public void writeTo(final RLPOutput output) {
134+
output.startList();
135+
writeConsensusRound(output);
136+
output.writeBytes(BLOCK.getHash().getBytes());
137+
// No blockAccessList field — simulates pre-26.1.0 encoding
138+
output.endList();
139+
}
140+
};
141+
142+
final SignedData<ProposalPayload> signedPayload =
143+
SignedData.create(
144+
oldFormatPayload,
145+
nodeKey.sign(Bytes32.wrap(oldFormatPayload.hashForSignature().getBytes())));
146+
147+
final Proposal proposal =
148+
Proposal.decode(new Proposal(signedPayload, List.of(), List.of()).encode(), blockEncoder);
149+
150+
assertThat(proposal.getBlockAccessList()).isEmpty();
151+
assertThat(proposal.getSignedPayload().getPayload().getRoundIdentifier())
152+
.isEqualTo(roundIdentifier);
153+
assertThat(proposal.getSignedPayload().getPayload().getProposedBlock().getHash())
154+
.isEqualTo(BLOCK.getHash());
155+
}
156+
107157
private void assertProposal(
108158
final Proposal decodedProposal,
109159
final Address expectedAddr,

0 commit comments

Comments
 (0)