diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bafb4c0d8..59e249d488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - \[EXPERIMENTAL\] Added support for using DNS host name in place of IP address in onchain node permissioning rules [#2667](https://github.com/hyperledger/besu/pull/2667) - Implement EIP-3607 Reject transactions from senders with deployed code. [#2676](https://github.com/hyperledger/besu/pull/2676) - Ignore all unknown fields when supplied to eth_estimateGas or eth_call. [\#2690](https://github.com/hyperledger/besu/pull/2690) +- \[EXPERIMENTAL\] Added support for QBFT with PKI-backed Block Creation. [#2647](https://github.com/hyperledger/besu/issues/2647) ### Bug Fixes - Consider effective price and effective priority fee in transaction replacement rules [\#2529](https://github.com/hyperledger/besu/issues/2529) diff --git a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/pki/PkiQbftAcceptanceTestParameterization.java b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/pki/PkiQbftAcceptanceTestParameterization.java index 5a191875af..96ffa7e4c3 100644 --- a/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/pki/PkiQbftAcceptanceTestParameterization.java +++ b/acceptance-tests/tests/src/test/java/org/hyperledger/besu/tests/acceptance/bft/pki/PkiQbftAcceptanceTestParameterization.java @@ -29,6 +29,12 @@ public class PkiQbftAcceptanceTestParameterization { final List ret = new ArrayList<>(); ret.addAll( List.of( + new Object[] { + "qbft-pki", + new PkiQbftAcceptanceTestParameterization( + BesuNodeFactory::createPkiQbftNode, + BesuNodeFactory::createPkiQbftNodeWithValidators) + }, new Object[] { "qbft-tls-jks", new PkiQbftAcceptanceTestParameterization( diff --git a/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java index 92e271296a..bda705542c 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java @@ -42,12 +42,12 @@ import org.hyperledger.besu.consensus.common.bft.statemachine.FutureMessageBuffe import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; import org.hyperledger.besu.consensus.common.validator.blockbased.BlockValidatorProvider; import org.hyperledger.besu.consensus.qbft.QbftBlockHeaderValidationRulesetFactory; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.QbftGossip; import org.hyperledger.besu.consensus.qbft.blockcreation.QbftBlockCreatorFactory; import org.hyperledger.besu.consensus.qbft.jsonrpc.QbftJsonRpcMethods; import org.hyperledger.besu.consensus.qbft.payload.MessageFactory; -import org.hyperledger.besu.consensus.qbft.pki.PkiQbftContext; import org.hyperledger.besu.consensus.qbft.pki.PkiQbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.protocol.Istanbul100SubProtocol; import org.hyperledger.besu.consensus.qbft.statemachine.QbftBlockHeightManagerFactory; @@ -299,15 +299,8 @@ public class QbftBesuControllerBuilder extends BftBesuControllerBuilder { validatorProvider = new TransactionValidatorProvider(blockchain, validatorContractController); } - if (pkiBlockCreationConfiguration.isPresent()) { - return new PkiQbftContext( - validatorProvider, - epochManager, - bftBlockInterface().get(), - pkiBlockCreationConfiguration.get()); - } else { - return new BftContext(validatorProvider, epochManager, bftBlockInterface().get()); - } + return new QbftContext( + validatorProvider, epochManager, bftBlockInterface().get(), pkiBlockCreationConfiguration); } private BftValidatorOverrides convertBftForks(final List bftForks) { diff --git a/consensus/common/src/test-support/java/org/hyperledger/besu/consensus/common/bft/BftContextBuilder.java b/consensus/common/src/test-support/java/org/hyperledger/besu/consensus/common/bft/BftContextBuilder.java index 302a0782a6..5abda4bbde 100644 --- a/consensus/common/src/test-support/java/org/hyperledger/besu/consensus/common/bft/BftContextBuilder.java +++ b/consensus/common/src/test-support/java/org/hyperledger/besu/consensus/common/bft/BftContextBuilder.java @@ -40,7 +40,14 @@ public class BftContextBuilder { public static BftContext setupContextWithBftExtraData( final Collection
validators, final BftExtraData bftExtraData) { - final BftContext bftContext = mock(BftContext.class, withSettings().lenient()); + return setupContextWithBftExtraData(BftContext.class, validators, bftExtraData); + } + + public static T setupContextWithBftExtraData( + final Class contextClazz, + final Collection
validators, + final BftExtraData bftExtraData) { + final T bftContext = mock(contextClazz, withSettings().lenient()); final ValidatorProvider mockValidatorProvider = mock(ValidatorProvider.class, withSettings().lenient()); final BftBlockInterface mockBftBlockInterface = @@ -54,7 +61,14 @@ public class BftContextBuilder { public static BftContext setupContextWithBftExtraDataEncoder( final Collection
validators, final BftExtraDataCodec bftExtraDataCodec) { - final BftContext bftContext = mock(BftContext.class, withSettings().lenient()); + return setupContextWithBftExtraDataEncoder(BftContext.class, validators, bftExtraDataCodec); + } + + public static T setupContextWithBftExtraDataEncoder( + final Class contextClazz, + final Collection
validators, + final BftExtraDataCodec bftExtraDataCodec) { + final T bftContext = mock(contextClazz, withSettings().lenient()); final ValidatorProvider mockValidatorProvider = mock(ValidatorProvider.class, withSettings().lenient()); when(bftContext.getValidatorProvider()).thenReturn(mockValidatorProvider); diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/BftExtraDataFixture.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/BftExtraDataFixture.java index 337bee1983..064ff10614 100644 --- a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/BftExtraDataFixture.java +++ b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/BftExtraDataFixture.java @@ -15,12 +15,15 @@ package org.hyperledger.besu.consensus.common.bft; import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; import org.hyperledger.besu.crypto.NodeKey; +import org.hyperledger.besu.crypto.NodeKeyUtils; import org.hyperledger.besu.crypto.SECPSignature; import org.hyperledger.besu.ethereum.core.Address; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Hash; +import org.hyperledger.besu.ethereum.core.Util; import java.util.List; import java.util.Optional; @@ -31,6 +34,22 @@ import org.apache.tuweni.bytes.Bytes; public class BftExtraDataFixture { + public static BftExtraData createExtraData( + final BlockHeader header, final BftExtraDataCodec bftExtraDataCodec) { + final NodeKey proposerNodeKey = NodeKeyUtils.generate(); + final Address proposerAddress = Util.publicKeyToAddress(proposerNodeKey.getPublicKey()); + final List
validators = singletonList(proposerAddress); + + return createExtraData( + header, + Bytes.wrap(new byte[BftExtraDataCodec.EXTRA_VANITY_LENGTH]), + Optional.of(Vote.authVote(Address.fromHexString("1"))), + validators, + singletonList(proposerNodeKey), + 0x2A, + bftExtraDataCodec); + } + public static BftExtraData createExtraData( final BlockHeader header, final Bytes vanityData, diff --git a/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/support/TestContextBuilder.java b/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/support/TestContextBuilder.java index 0302d6047a..bbd46779ae 100644 --- a/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/support/TestContextBuilder.java +++ b/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/support/TestContextBuilder.java @@ -50,6 +50,7 @@ import org.hyperledger.besu.consensus.common.bft.statemachine.FutureMessageBuffe import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; import org.hyperledger.besu.consensus.common.validator.blockbased.BlockValidatorProvider; import org.hyperledger.besu.consensus.qbft.QbftBlockHeaderValidationRulesetFactory; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.QbftGossip; import org.hyperledger.besu.consensus.qbft.blockcreation.QbftBlockCreatorFactory; @@ -400,7 +401,7 @@ public class TestContextBuilder { new ProtocolContext( blockChain, worldStateArchive, - new BftContext(validatorProvider, epochManager, blockInterface)); + new QbftContext(validatorProvider, epochManager, blockInterface, Optional.empty())); final GasPricePendingTransactionsSorter pendingTransactions = new GasPricePendingTransactionsSorter( diff --git a/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/test/round/QbftRoundIntegrationTest.java b/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/test/round/QbftRoundIntegrationTest.java index f517255117..29de5b0f06 100644 --- a/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/test/round/QbftRoundIntegrationTest.java +++ b/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/test/round/QbftRoundIntegrationTest.java @@ -29,6 +29,7 @@ import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.RoundTimer; import org.hyperledger.besu.consensus.common.bft.blockcreation.BftBlockCreator; import org.hyperledger.besu.consensus.common.bft.inttest.StubValidatorMulticaster; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.network.QbftMessageTransmitter; import org.hyperledger.besu.consensus.qbft.payload.MessageFactory; @@ -115,7 +116,8 @@ public class QbftRoundIntegrationTest { new ProtocolContext( blockChain, worldStateArchive, - setupContextWithBftExtraDataEncoder(emptyList(), qbftExtraDataEncoder)); + setupContextWithBftExtraDataEncoder( + QbftContext.class, emptyList(), qbftExtraDataEncoder)); } @Test diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftContext.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftContext.java similarity index 71% rename from consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftContext.java rename to consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftContext.java index 13ea947335..5c54fda1ac 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftContext.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftContext.java @@ -12,28 +12,30 @@ * * SPDX-License-Identifier: Apache-2.0 */ - -package org.hyperledger.besu.consensus.qbft.pki; +package org.hyperledger.besu.consensus.qbft; import org.hyperledger.besu.consensus.common.EpochManager; import org.hyperledger.besu.consensus.common.bft.BftBlockInterface; import org.hyperledger.besu.consensus.common.bft.BftContext; import org.hyperledger.besu.consensus.common.validator.ValidatorProvider; +import org.hyperledger.besu.consensus.qbft.pki.PkiBlockCreationConfiguration; + +import java.util.Optional; -public class PkiQbftContext extends BftContext { +public class QbftContext extends BftContext { - private final PkiBlockCreationConfiguration pkiBlockCreationConfiguration; + private final Optional pkiBlockCreationConfiguration; - public PkiQbftContext( + public QbftContext( final ValidatorProvider validatorProvider, final EpochManager epochManager, final BftBlockInterface blockInterface, - final PkiBlockCreationConfiguration pkiBlockCreationConfiguration) { + final Optional pkiBlockCreationConfiguration) { super(validatorProvider, epochManager, blockInterface); this.pkiBlockCreationConfiguration = pkiBlockCreationConfiguration; } - public PkiBlockCreationConfiguration getPkiBlockCreationConfiguration() { + public Optional getPkiBlockCreationConfiguration() { return pkiBlockCreationConfiguration; } } diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java index aea2b93dc9..2579c8a87c 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftExtraDataCodec.java @@ -87,7 +87,7 @@ public class QbftExtraDataCodec extends BftExtraDataCodec { final List seals = rlpInput.readList( rlp -> SignatureAlgorithmFactory.getInstance().decodeSignature(rlp.readBytes())); - rlpInput.leaveList(); + rlpInput.leaveListLenient(); return new BftExtraData(vanityData, seals, vote, round, validators); } diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHashing.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHashing.java new file mode 100644 index 0000000000..1d222064ce --- /dev/null +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHashing.java @@ -0,0 +1,37 @@ +/* + * Copyright ConsenSys AG. + * + * 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.pki; + +import org.hyperledger.besu.consensus.common.bft.BftBlockHashing; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.Hash; + +public class PkiQbftBlockHashing { + + private final PkiQbftExtraDataCodec extraDataCodec; + + public PkiQbftBlockHashing(final PkiQbftExtraDataCodec extraDataCodec) { + this.extraDataCodec = extraDataCodec; + } + + public Hash calculateHashOfBftBlockForCmsSignature(final BlockHeader header) { + final BftExtraData bftExtraData = extraDataCodec.decode(header); + return Hash.hash( + BftBlockHashing.serializeHeader( + header, () -> extraDataCodec.encodeWithoutCms(bftExtraData), extraDataCodec)); + } +} diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHeaderFunctions.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHeaderFunctions.java new file mode 100644 index 0000000000..061eecc908 --- /dev/null +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHeaderFunctions.java @@ -0,0 +1,29 @@ +/* + * Copyright ConsenSys AG. + * + * 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.pki; + +import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions; +import org.hyperledger.besu.ethereum.core.BlockHeaderFunctions; + +public class PkiQbftBlockHeaderFunctions { + + public static BlockHeaderFunctions forCmsSignature( + final PkiQbftExtraDataCodec bftExtraDataCodec) { + return new BftBlockHeaderFunctions( + h -> new PkiQbftBlockHashing(bftExtraDataCodec).calculateHashOfBftBlockForCmsSignature(h), + bftExtraDataCodec); + } +} diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftCreateBlockForProposalBehaviour.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftCreateBlockForProposalBehaviour.java new file mode 100644 index 0000000000..053d9bad21 --- /dev/null +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftCreateBlockForProposalBehaviour.java @@ -0,0 +1,97 @@ +/* + * Copyright ConsenSys AG. + * + * 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.pki; + +import static com.google.common.base.Preconditions.checkArgument; + +import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; +import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec; +import org.hyperledger.besu.consensus.qbft.statemachine.CreateBlockForProposalBehaviour; +import org.hyperledger.besu.ethereum.blockcreation.BlockCreator; +import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder; +import org.hyperledger.besu.ethereum.core.Hash; +import org.hyperledger.besu.pki.cms.CmsCreator; + +import com.google.common.annotations.VisibleForTesting; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.tuweni.bytes.Bytes; + +public class PkiQbftCreateBlockForProposalBehaviour implements CreateBlockForProposalBehaviour { + + private static final Logger LOG = LogManager.getLogger(); + + private final BlockCreator blockCreator; + private final CmsCreator cmsCreator; + private final PkiQbftExtraDataCodec bftExtraDataCodec; + + public PkiQbftCreateBlockForProposalBehaviour( + final BlockCreator blockCreator, + final PkiBlockCreationConfiguration pkiBlockCreationConfiguration, + final BftExtraDataCodec bftExtraDataCodec) { + this( + blockCreator, + new CmsCreator( + pkiBlockCreationConfiguration.getKeyStore(), + pkiBlockCreationConfiguration.getCertificateAlias()), + bftExtraDataCodec); + } + + @VisibleForTesting + PkiQbftCreateBlockForProposalBehaviour( + final BlockCreator blockCreator, + final CmsCreator cmsCreator, + final BftExtraDataCodec bftExtraDataCodec) { + this.blockCreator = blockCreator; + this.cmsCreator = cmsCreator; + + checkArgument( + bftExtraDataCodec instanceof PkiQbftExtraDataCodec, + "PkiQbftCreateBlockForProposalBehaviour must use PkiQbftExtraDataCodec"); + this.bftExtraDataCodec = (PkiQbftExtraDataCodec) bftExtraDataCodec; + } + + @Override + public Block create(final long headerTimeStampSeconds) { + final Block block = blockCreator.createBlock(headerTimeStampSeconds); + return replaceCmsInBlock(block); + } + + private Block replaceCmsInBlock(final Block block) { + final BlockHeader blockHeader = block.getHeader(); + final Hash hashWithoutCms = + PkiQbftBlockHeaderFunctions.forCmsSignature(bftExtraDataCodec).hash(block.getHeader()); + + final Bytes cms = cmsCreator.create(hashWithoutCms); + + final BftExtraData previousExtraData = bftExtraDataCodec.decode(blockHeader); + final BftExtraData substituteExtraData = new PkiQbftExtraData(previousExtraData, cms); + final Bytes substituteExtraDataBytes = bftExtraDataCodec.encode(substituteExtraData); + + final BlockHeaderBuilder headerBuilder = BlockHeaderBuilder.fromHeader(blockHeader); + headerBuilder + .extraData(substituteExtraDataBytes) + .blockHeaderFunctions(BftBlockHeaderFunctions.forCommittedSeal(bftExtraDataCodec)); + final BlockHeader newHeader = headerBuilder.buildBlockHeader(); + + LOG.debug("Created CMS with signed hash {} for block {}", hashWithoutCms, newHeader.getHash()); + + return new Block(newHeader, block.getBody()); + } +} diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraData.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraData.java index 64bc1ba889..a8ad42fe2b 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraData.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraData.java @@ -27,7 +27,7 @@ import org.apache.tuweni.bytes.Bytes; public class PkiQbftExtraData extends BftExtraData { - private final Optional cms; + private final Bytes cms; public PkiQbftExtraData( final Bytes vanityData, @@ -35,12 +35,12 @@ public class PkiQbftExtraData extends BftExtraData { final Optional vote, final int round, final Collection
validators, - final Optional cms) { + final Bytes cms) { super(vanityData, seals, vote, round, validators); this.cms = cms; } - PkiQbftExtraData(final BftExtraData bftExtraData, final Optional cms) { + PkiQbftExtraData(final BftExtraData bftExtraData, final Bytes cms) { this( bftExtraData.getVanityData(), bftExtraData.getSeals(), @@ -50,7 +50,7 @@ public class PkiQbftExtraData extends BftExtraData { cms); } - public Optional getCms() { + public Bytes getCms() { return cms; } } diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraDataCodec.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraDataCodec.java index 6cc680c30a..b941526638 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraDataCodec.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraDataCodec.java @@ -1,13 +1,16 @@ /* * Copyright ConsenSys AG. * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * 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 + * 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 @@ -20,37 +23,35 @@ import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.RLPInput; -import java.util.Optional; +import java.util.List; import org.apache.tuweni.bytes.Bytes; /* - The PkiQbftExtraData encoding format is different from the "regular" QbftExtraData encoding. - We have an "envelope" list, with two elements: the extra data and the cms message. - The RLP encoding format is as follows: ["extra_data", ["cms"]] + The PkiQbftExtraData encoding format is different from the "regular" QbftExtraData encoding. We + have an extra bytes element in the end of the list. */ public class PkiQbftExtraDataCodec extends QbftExtraDataCodec { + public static final int QBFT_EXTRA_DATA_LIST_SIZE = 5; + @Override public BftExtraData decodeRaw(final Bytes input) { if (input.isEmpty()) { throw new IllegalArgumentException("Invalid Bytes supplied - Bft Extra Data required."); } - final RLPInput rlpInput = new BytesValueRLPInput(input, false); - rlpInput.enterList(); + final BftExtraData bftExtraData = super.decodeRaw(input); - // Consume all the ExtraData input from the envelope list, and decode it using the QBFT decoder - final Bytes extraDataListAsBytes = rlpInput.currentListAsBytes(); - final BftExtraData bftExtraData = super.decodeRaw(extraDataListAsBytes); + final RLPInput rlpInput = new BytesValueRLPInput(input, false); - final Optional cms; - if (rlpInput.nextIsList() && rlpInput.nextSize() == 0) { - cms = Optional.empty(); + final Bytes cms; + final List elements = rlpInput.readList(RLPInput::readAsRlp); + if (elements.size() > QBFT_EXTRA_DATA_LIST_SIZE) { + final RLPInput cmsElement = elements.get(elements.size() - 1); + cms = cmsElement.readBytes(); } else { - rlpInput.enterList(); - cms = Optional.of(rlpInput.readBytes()); - rlpInput.leaveList(); + cms = Bytes.EMPTY; } return new PkiQbftExtraData(bftExtraData, cms); @@ -58,33 +59,30 @@ public class PkiQbftExtraDataCodec extends QbftExtraDataCodec { @Override protected Bytes encode(final BftExtraData bftExtraData, final EncodingType encodingType) { - if (!(bftExtraData instanceof PkiQbftExtraData)) { - throw new IllegalStateException( - "PkiQbftExtraDataCodec must be used only with PkiQbftExtraData"); - } - final PkiQbftExtraData extraData = (PkiQbftExtraData) bftExtraData; - - final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); - encoder.startList(); // start envelope list - - final Bytes encodedQbftExtraData = super.encode(bftExtraData, encodingType); - encoder.writeRaw(encodedQbftExtraData); + return encode(bftExtraData, encodingType, true); + } - if (encodingType == EncodingType.ALL) { - if (extraData.getCms().isPresent()) { - Bytes cmsBytes = extraData.getCms().get(); - encoder.startList(); - encoder.writeBytes(cmsBytes); - encoder.endList(); - } else { - encoder.writeEmptyList(); - } - } else { - encoder.writeEmptyList(); + private Bytes encode( + final BftExtraData bftExtraData, final EncodingType encodingType, final boolean includeCms) { + final Bytes encoded = super.encode(bftExtraData, encodingType); + if (!(bftExtraData instanceof PkiQbftExtraData) || !includeCms) { + return encoded; } - encoder.endList(); // end envelope list + final BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); + rlpOutput.startList(); + // Read through extraData RLP list elements and write them to the new RLP output + new BytesValueRLPInput(encoded, false) + .readList(RLPInput::readAsRlp).stream() + .map(RLPInput::raw) + .forEach(rlpOutput::writeRLPBytes); + rlpOutput.writeBytes(((PkiQbftExtraData) bftExtraData).getCms()); + rlpOutput.endList(); + + return rlpOutput.encoded(); + } - return encoder.encoded(); + public Bytes encodeWithoutCms(final BftExtraData bftExtraData) { + return encode(bftExtraData, EncodingType.ALL, false); } } diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/CreateBlockForProposalBehaviour.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/CreateBlockForProposalBehaviour.java new file mode 100644 index 0000000000..0373e83686 --- /dev/null +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/CreateBlockForProposalBehaviour.java @@ -0,0 +1,24 @@ +/* + * Copyright ConsenSys AG. + * + * 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.statemachine; + +import org.hyperledger.besu.ethereum.core.Block; + +@FunctionalInterface +public interface CreateBlockForProposalBehaviour { + + Block create(long headerTimeStampSeconds); +} diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java index 5601afba3b..dd3dc3ada2 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRound.java @@ -49,6 +49,7 @@ import org.hyperledger.besu.util.Subscribers; import java.util.List; import java.util.Optional; +import com.google.common.annotations.VisibleForTesting; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -58,15 +59,17 @@ public class QbftRound { private static final Logger LOG = LogManager.getLogger(); private final Subscribers observers; - private final RoundState roundState; - private final BftBlockCreator blockCreator; - private final ProtocolContext protocolContext; + protected final RoundState roundState; + protected final BftBlockCreator blockCreator; + protected final ProtocolContext protocolContext; private final BlockImporter blockImporter; private final NodeKey nodeKey; private final MessageFactory messageFactory; // used only to create stored local msgs private final QbftMessageTransmitter transmitter; - private final BftExtraDataCodec bftExtraDataCodec; + protected final BftExtraDataCodec bftExtraDataCodec; + protected CreateBlockForProposalBehaviour createBlockForProposalBehaviour; + @VisibleForTesting public QbftRound( final RoundState roundState, final BftBlockCreator blockCreator, @@ -78,6 +81,32 @@ public class QbftRound { final QbftMessageTransmitter transmitter, final RoundTimer roundTimer, final BftExtraDataCodec bftExtraDataCodec) { + this( + roundState, + blockCreator, + protocolContext, + blockImporter, + observers, + nodeKey, + messageFactory, + transmitter, + roundTimer, + bftExtraDataCodec, + blockCreator::createBlock); + } + + public QbftRound( + final RoundState roundState, + final BftBlockCreator blockCreator, + final ProtocolContext protocolContext, + final BlockImporter blockImporter, + final Subscribers observers, + final NodeKey nodeKey, + final MessageFactory messageFactory, + final QbftMessageTransmitter transmitter, + final RoundTimer roundTimer, + final BftExtraDataCodec bftExtraDataCodec, + final CreateBlockForProposalBehaviour createBlockForProposalBehaviour) { this.roundState = roundState; this.blockCreator = blockCreator; this.protocolContext = protocolContext; @@ -87,6 +116,7 @@ public class QbftRound { this.messageFactory = messageFactory; this.transmitter = transmitter; this.bftExtraDataCodec = bftExtraDataCodec; + this.createBlockForProposalBehaviour = createBlockForProposalBehaviour; roundTimer.startTimer(getRoundIdentifier()); } @@ -96,8 +126,9 @@ public class QbftRound { } public void createAndSendProposalMessage(final long headerTimeStampSeconds) { - final Block block = blockCreator.createBlock(headerTimeStampSeconds); LOG.debug("Creating proposed block. round={}", roundState.getRoundIdentifier()); + final Block block = createBlockForProposalBehaviour.create(headerTimeStampSeconds); + LOG.trace("Creating proposed block blockHeader={}", block.getHeader()); updateStateWithProposalAndTransmit(block, emptyList(), emptyList()); } @@ -110,7 +141,7 @@ public class QbftRound { Block blockToPublish; if (bestPreparedCertificate.isEmpty()) { LOG.debug("Sending proposal with new block. round={}", roundState.getRoundIdentifier()); - blockToPublish = blockCreator.createBlock(headerTimestamp); + blockToPublish = createBlockForProposalBehaviour.create(headerTimestamp); } else { LOG.debug( "Sending proposal from PreparedCertificate. round={}", roundState.getRoundIdentifier()); @@ -123,7 +154,7 @@ public class QbftRound { bestPreparedCertificate.map(PreparedCertificate::getPrepares).orElse(emptyList())); } - private void updateStateWithProposalAndTransmit( + protected void updateStateWithProposalAndTransmit( final Block block, final List> roundChanges, final List> prepares) { diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundFactory.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundFactory.java index 3a806c7354..5a4a48a2dd 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundFactory.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundFactory.java @@ -19,8 +19,10 @@ import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.blockcreation.BftBlockCreator; import org.hyperledger.besu.consensus.common.bft.blockcreation.BftBlockCreatorFactory; import org.hyperledger.besu.consensus.common.bft.statemachine.BftFinalState; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.network.QbftMessageTransmitter; import org.hyperledger.besu.consensus.qbft.payload.MessageFactory; +import org.hyperledger.besu.consensus.qbft.pki.PkiQbftCreateBlockForProposalBehaviour; import org.hyperledger.besu.consensus.qbft.validation.MessageValidatorFactory; import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.chain.MinedBlockObserver; @@ -80,6 +82,18 @@ public class QbftRoundFactory { final QbftMessageTransmitter messageTransmitter = new QbftMessageTransmitter(messageFactory, finalState.getValidatorMulticaster()); + final QbftContext qbftContext = protocolContext.getConsensusState(QbftContext.class); + final CreateBlockForProposalBehaviour createBlockForProposalBehaviour; + if (qbftContext.getPkiBlockCreationConfiguration().isPresent()) { + createBlockForProposalBehaviour = + new PkiQbftCreateBlockForProposalBehaviour( + blockCreator, + qbftContext.getPkiBlockCreationConfiguration().get(), + bftExtraDataCodec); + } else { + createBlockForProposalBehaviour = blockCreator::createBlock; + } + return new QbftRound( roundState, blockCreator, @@ -90,6 +104,7 @@ public class QbftRoundFactory { messageFactory, messageTransmitter, finalState.getRoundTimer(), - bftExtraDataCodec); + bftExtraDataCodec, + createBlockForProposalBehaviour); } } diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/validation/ProposalPayloadValidator.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/validation/ProposalPayloadValidator.java index 6b47f94683..8fd622f8fd 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/validation/ProposalPayloadValidator.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/validation/ProposalPayloadValidator.java @@ -14,18 +14,27 @@ */ package org.hyperledger.besu.consensus.qbft.validation; +import org.hyperledger.besu.consensus.common.bft.BftBlockInterface; +import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.payload.SignedData; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.payload.ProposalPayload; +import org.hyperledger.besu.consensus.qbft.pki.PkiQbftBlockHeaderFunctions; +import org.hyperledger.besu.consensus.qbft.pki.PkiQbftExtraData; +import org.hyperledger.besu.consensus.qbft.pki.PkiQbftExtraDataCodec; import org.hyperledger.besu.ethereum.BlockValidator; import org.hyperledger.besu.ethereum.BlockValidator.BlockProcessingOutputs; import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.core.Address; import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.Hash; import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; +import org.hyperledger.besu.pki.cms.CmsValidator; import java.util.Optional; +import com.google.common.annotations.VisibleForTesting; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -38,16 +47,41 @@ public class ProposalPayloadValidator { private final ConsensusRoundIdentifier targetRound; private final BlockValidator blockValidator; private final ProtocolContext protocolContext; + private final BftExtraDataCodec bftExtraDataCodec; + private final Optional cmsValidator; public ProposalPayloadValidator( final Address expectedProposer, final ConsensusRoundIdentifier targetRound, final BlockValidator blockValidator, - final ProtocolContext protocolContext) { + final ProtocolContext protocolContext, + final BftExtraDataCodec bftExtraDataCodec) { + this( + expectedProposer, + targetRound, + blockValidator, + protocolContext, + bftExtraDataCodec, + protocolContext + .getConsensusState(QbftContext.class) + .getPkiBlockCreationConfiguration() + .map(config -> new CmsValidator(config.getTrustStore()))); + } + + @VisibleForTesting + public ProposalPayloadValidator( + final Address expectedProposer, + final ConsensusRoundIdentifier targetRound, + final BlockValidator blockValidator, + final ProtocolContext protocolContext, + final BftExtraDataCodec bftExtraDataCodec, + final Optional cmsValidator) { this.expectedProposer = expectedProposer; this.targetRound = targetRound; this.blockValidator = blockValidator; this.protocolContext = protocolContext; + this.bftExtraDataCodec = bftExtraDataCodec; + this.cmsValidator = cmsValidator; } public boolean validate(final SignedData signedPayload) { @@ -74,6 +108,13 @@ public class ProposalPayloadValidator { return false; } + if (cmsValidator.isPresent()) { + return validateCms( + block, + protocolContext.getConsensusState(QbftContext.class).getBlockInterface(), + cmsValidator.get()); + } + return true; } @@ -89,4 +130,26 @@ public class ProposalPayloadValidator { return true; } + + private boolean validateCms( + final Block block, + final BftBlockInterface bftBlockInterface, + final CmsValidator cmsValidator) { + final PkiQbftExtraData pkiExtraData = + (PkiQbftExtraData) bftBlockInterface.getExtraData(block.getHeader()); + + final Hash hashWithoutCms = + PkiQbftBlockHeaderFunctions.forCmsSignature((PkiQbftExtraDataCodec) bftExtraDataCodec) + .hash(block.getHeader()); + + LOG.debug("Validating CMS with signed hash {} in block {}", hashWithoutCms, block.getHash()); + + if (!cmsValidator.validate(pkiExtraData.getCms(), hashWithoutCms)) { + LOG.info("{}: invalid CMS in block {}", ERROR_PREFIX, block.getHash()); + return false; + } else { + LOG.trace("Valid CMS in block {}", block.getHash()); + return true; + } + } } diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/validation/ProposalValidator.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/validation/ProposalValidator.java index e0baa44fe1..75e63d8dec 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/validation/ProposalValidator.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/validation/ProposalValidator.java @@ -77,7 +77,7 @@ public class ProposalValidator { final ProposalPayloadValidator payloadValidator = new ProposalPayloadValidator( - expectedProposer, roundIdentifier, blockValidator, protocolContext); + expectedProposer, roundIdentifier, blockValidator, protocolContext, bftExtraDataCodec); if (!payloadValidator.validate(msg.getSignedPayload())) { LOG.info("{}: invalid proposal payload in proposal message", ERROR_PREFIX); diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactoryTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactoryTest.java index 7478914c04..362473ad70 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactoryTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactoryTest.java @@ -47,7 +47,10 @@ public class QbftBlockHeaderValidationRulesetFactoryTest { private ProtocolContext protocolContext(final Collection
validators) { return new ProtocolContext( - null, null, setupContextWithBftExtraDataEncoder(validators, new QbftExtraDataCodec())); + null, + null, + setupContextWithBftExtraDataEncoder( + QbftContext.class, validators, new QbftExtraDataCodec())); } @Test diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/QbftValidatorsValidationRuleTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/QbftValidatorsValidationRuleTest.java index d94c1b812a..75b6da3362 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/QbftValidatorsValidationRuleTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/headervalidationrules/QbftValidatorsValidationRuleTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.when; import org.hyperledger.besu.consensus.common.bft.BftExtraData; import org.hyperledger.besu.consensus.common.bft.Vote; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.core.Address; import org.hyperledger.besu.ethereum.core.AddressHelpers; @@ -43,7 +44,9 @@ public class QbftValidatorsValidationRuleTest { new QbftValidatorsValidationRule(true); final ProtocolContext context = new ProtocolContext( - null, null, setupContextWithBftExtraData(Collections.emptyList(), bftExtraData)); + null, + null, + setupContextWithBftExtraData(QbftContext.class, Collections.emptyList(), bftExtraData)); when(bftExtraData.getValidators()).thenReturn(Collections.emptyList()); when(bftExtraData.getVote()).thenReturn(Optional.empty()); assertThat(qbftValidatorsValidationRule.validate(blockHeader, null, context)).isTrue(); @@ -58,7 +61,8 @@ public class QbftValidatorsValidationRuleTest { AddressHelpers.ofValue(1), AddressHelpers.ofValue(2), AddressHelpers.ofValue(3)); final ProtocolContext context = - new ProtocolContext(null, null, setupContextWithBftExtraData(validators, bftExtraData)); + new ProtocolContext( + null, null, setupContextWithBftExtraData(QbftContext.class, validators, bftExtraData)); when(bftExtraData.getValidators()).thenReturn(validators); assertThat(qbftValidatorsValidationRule.validate(blockHeader, null, context)).isTrue(); } @@ -72,7 +76,8 @@ public class QbftValidatorsValidationRuleTest { AddressHelpers.ofValue(1), AddressHelpers.ofValue(2), AddressHelpers.ofValue(3)); final ProtocolContext context = - new ProtocolContext(null, null, setupContextWithBftExtraData(validators, bftExtraData)); + new ProtocolContext( + null, null, setupContextWithBftExtraData(QbftContext.class, validators, bftExtraData)); when(bftExtraData.getValidators()).thenReturn(validators); assertThat(qbftValidatorsValidationRule.validate(blockHeader, null, context)).isFalse(); } @@ -83,7 +88,9 @@ public class QbftValidatorsValidationRuleTest { new QbftValidatorsValidationRule(true); final ProtocolContext context = new ProtocolContext( - null, null, setupContextWithBftExtraData(Collections.emptyList(), bftExtraData)); + null, + null, + setupContextWithBftExtraData(QbftContext.class, Collections.emptyList(), bftExtraData)); when(bftExtraData.getValidators()).thenReturn(Collections.emptyList()); when(bftExtraData.getVote()).thenReturn(Optional.of(mock(Vote.class))); assertThat(qbftValidatorsValidationRule.validate(blockHeader, null, context)).isFalse(); diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHashingTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHashingTest.java new file mode 100644 index 0000000000..15e57de53e --- /dev/null +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftBlockHashingTest.java @@ -0,0 +1,85 @@ +/* + * Copyright ConsenSys AG. + * + * 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.pki; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.hyperledger.besu.consensus.common.bft.BftBlockHashing; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; +import org.hyperledger.besu.consensus.common.bft.BftExtraDataFixture; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.Hash; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.Before; +import org.junit.Test; + +public class PkiQbftBlockHashingTest { + + private PkiQbftExtraDataCodec pkiExtraDataCodec = new PkiQbftExtraDataCodec(); + private PkiQbftBlockHashing pkiQbftBlockHashing; + + @Before + public void before() { + pkiExtraDataCodec = spy(new PkiQbftExtraDataCodec()); + pkiQbftBlockHashing = new PkiQbftBlockHashing(pkiExtraDataCodec); + } + + @Test + public void blockHashingUsesCorrectEncodingWithoutCmsMethodInCodec() { + final PkiQbftExtraData pkiQbftExtraData = createPkiQbftExtraData(); + final BlockHeader headerWithExtraData = + new BlockHeaderTestFixture() + .number(1L) + .extraData(pkiExtraDataCodec.encode(pkiQbftExtraData)) + .buildHeader(); + + // Expected hash using the extraData encoded by the encodeWithoutCms method of the codec + final Hash expectedHash = + Hash.hash( + BftBlockHashing.serializeHeader( + headerWithExtraData, + () -> pkiExtraDataCodec.encodeWithoutCms(pkiQbftExtraData), + pkiExtraDataCodec)); + + final Hash hash = + pkiQbftBlockHashing.calculateHashOfBftBlockForCmsSignature(headerWithExtraData); + + assertThat(hash).isEqualTo(expectedHash); + + /* + Verify that the encodeWithoutCms method was called twice, once when calculating the + expected hash and a second time as part of the hash calculation on + calculateHashOfBftBlockForCmsSignature + */ + verify(pkiExtraDataCodec, times(2)).encodeWithoutCms(any(PkiQbftExtraData.class)); + } + + private PkiQbftExtraData createPkiQbftExtraData() { + final BlockHeader blockHeader = new BlockHeaderTestFixture().buildHeader(); + final BftExtraData extraData = + BftExtraDataFixture.createExtraData(blockHeader, pkiExtraDataCodec); + return new PkiQbftExtraData(extraData, Bytes.random(32)); + } +} diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftCreateBlockForProposalBehaviourTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftCreateBlockForProposalBehaviourTest.java new file mode 100644 index 0000000000..66648ebd08 --- /dev/null +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftCreateBlockForProposalBehaviourTest.java @@ -0,0 +1,132 @@ +/* + * Copyright ConsenSys AG. + * + * 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.pki; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.hyperledger.besu.consensus.common.bft.BftExtraDataFixture.createExtraData; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; +import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; +import org.hyperledger.besu.ethereum.blockcreation.BlockCreator; +import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.BlockBody; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.Hash; +import org.hyperledger.besu.pki.cms.CmsCreator; + +import java.util.Collections; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.Before; +import org.junit.Test; + +public class PkiQbftCreateBlockForProposalBehaviourTest { + + private final PkiQbftExtraDataCodec extraDataCodec = new PkiQbftExtraDataCodec(); + + private BlockCreator blockCreator; + private CmsCreator cmsCreator; + private PkiQbftCreateBlockForProposalBehaviour createBlockForProposalBehaviour; + private BlockHeaderTestFixture blockHeaderBuilder; + + @Before + public void before() { + blockCreator = mock(BlockCreator.class); + cmsCreator = mock(CmsCreator.class); + + createBlockForProposalBehaviour = + new PkiQbftCreateBlockForProposalBehaviour(blockCreator, cmsCreator, extraDataCodec); + + blockHeaderBuilder = new BlockHeaderTestFixture(); + } + + @Test + public void createProposalBehaviourWithNonPkiCodecFails() { + assertThatThrownBy( + () -> + new PkiQbftCreateBlockForProposalBehaviour( + blockCreator, cmsCreator, new QbftExtraDataCodec())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "PkiQbftCreateBlockForProposalBehaviour must use PkiQbftExtraDataCodec"); + } + + @Test + public void cmsInProposedBlockHasValueCreatedByCmsCreator() { + createBlockBeingProposed(); + + final Bytes cms = Bytes.random(32); + when(cmsCreator.create(any(Bytes.class))).thenReturn(cms); + + final Block proposedBlock = createBlockForProposalBehaviour.create(1L); + + final PkiQbftExtraData proposedBlockExtraData = + (PkiQbftExtraData) extraDataCodec.decodeRaw(proposedBlock.getHeader().getExtraData()); + assertThat(proposedBlockExtraData).isInstanceOf(PkiQbftExtraData.class); + assertThat(proposedBlockExtraData.getCms()).isEqualTo(cms); + } + + @Test + public void cmsIsCreatedWithCorrectHashingFunction() { + final Block block = createBlockBeingProposed(); + final Hash expectedHashForCmsCreation = + PkiQbftBlockHeaderFunctions.forCmsSignature(extraDataCodec).hash(block.getHeader()); + + when(cmsCreator.create(any(Bytes.class))).thenReturn(Bytes.random(32)); + + createBlockForProposalBehaviour.create(1L); + + verify(cmsCreator).create(eq(expectedHashForCmsCreation)); + } + + @Test + public void proposedBlockHashUsesCommittedSealHeaderFunction() { + createBlockBeingProposed(); + when(cmsCreator.create(any(Bytes.class))).thenReturn(Bytes.random(32)); + + final Block blockWithCms = createBlockForProposalBehaviour.create(1L); + + final Hash expectedBlockHash = + BftBlockHeaderFunctions.forCommittedSeal(extraDataCodec).hash(blockWithCms.getHeader()); + + assertThat(blockWithCms.getHash()).isEqualTo(expectedBlockHash); + } + + private Block createBlockBeingProposed() { + final BftExtraData originalExtraData = + createExtraData(blockHeaderBuilder.buildHeader(), extraDataCodec); + final BlockHeader blockHeaderWithExtraData = + blockHeaderBuilder.extraData(extraDataCodec.encode(originalExtraData)).buildHeader(); + final Block block = + new Block( + blockHeaderWithExtraData, + new BlockBody(Collections.emptyList(), Collections.emptyList())); + when(blockCreator.createBlock(eq(1L))).thenReturn(block); + + return block; + } +} diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraDataCodecTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraDataCodecTest.java index de15c8da00..d7ecf21f06 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraDataCodecTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/pki/PkiQbftExtraDataCodecTest.java @@ -1,5 +1,5 @@ /* - * Copyright ConsenSys AG. + * Copyright 2020 ConsenSys AG. * * 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 @@ -18,6 +18,7 @@ package org.hyperledger.besu.consensus.qbft.pki; import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.consensus.qbft.QbftExtraDataCodecTestUtils.createNonEmptyVanityData; +import org.hyperledger.besu.consensus.common.bft.BftExtraData; import org.hyperledger.besu.consensus.common.bft.Vote; import org.hyperledger.besu.crypto.SECPSignature; import org.hyperledger.besu.crypto.SignatureAlgorithm; @@ -41,25 +42,34 @@ public class PkiQbftExtraDataCodecTest { private static final Supplier SIGNATURE_ALGORITHM = Suppliers.memoize(SignatureAlgorithmFactory::getInstance); - private final String EMPTY_CMS_RAW_HEX_ENCODING_STRING = - "0xf8f3f8f0a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea94000000000000" - + "0000000000000000000000000001940000000000000000000000000000000000000002d794000000000000" - + "000000000000000000000000000181ff83fedcbaf886b84100000000000000000000000000000000000000" - + "00000000000000000000000001000000000000000000000000000000000000000000000000000000000000" - + "000a00b841000000000000000000000000000000000000000000000000000000000000000a000000000000" - + "000000000000000000000000000000000000000000000000000100c0"; - // Arbitrary bytes representing a non-empty CMS private final Bytes cms = Bytes.fromHexString("0x01"); - // Raw hex-encoded extra data with arbitrary CMS data (0x01) - private final String RAW_HEX_ENCODING_STRING = - "0xf8f4f8f0a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea94000000000000" - + "0000000000000000000000000001940000000000000000000000000000000000000002d794000000000000" - + "000000000000000000000000000181ff83fedcbaf886b84100000000000000000000000000000000000000" - + "00000000000000000000000001000000000000000000000000000000000000000000000000000000000000" - + "000a00b841000000000000000000000000000000000000000000000000000000000000000a000000000000" - + "000000000000000000000000000000000000000000000000000100c101"; + private final String RAW_EXCLUDE_COMMIT_SEALS_AND_ROUND_NUMBER_ENCODED_STRING = + "0xf867a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea940000000000000000" + + "000000000000000000000001940000000000000000000000000000000000000002d7940000000000000000" + + "00000000000000000000000181ff80c001"; + + private final String RAW_EXCLUDE_COMMIT_SEALS_ENCODED_STRING = + "0xf86aa00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea940000000000000000" + + "000000000000000000000001940000000000000000000000000000000000000002d7940000000000000000" + + "00000000000000000000000181ff83fedcbac001"; + + private final String RAW_ALL_ENCODED_STRING = + "0xf8f1a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea940000000000000000" + + "000000000000000000000001940000000000000000000000000000000000000002d7940000000000000000" + + "00000000000000000000000181ff83fedcbaf886b841000000000000000000000000000000000000000000" + + "0000000000000000000001000000000000000000000000000000000000000000000000000000000000000a" + + "00b841000000000000000000000000000000000000000000000000000000000000000a0000000000000000" + + "0000000000000000000000000000000000000000000000010001"; + + private final String RAW_QBFT_EXTRA_DATA = + "0xf8f0a00102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20ea940000000000000000" + + "000000000000000000000001940000000000000000000000000000000000000002d7940000000000000000" + + "00000000000000000000000181ff83fedcbaf886b841000000000000000000000000000000000000000000" + + "0000000000000000000001000000000000000000000000000000000000000000000000000000000000000a" + + "00b841000000000000000000000000000000000000000000000000000000000000000a0000000000000000" + + "00000000000000000000000000000000000000000000000100"; private final PkiQbftExtraDataCodec bftExtraDataCodec = new PkiQbftExtraDataCodec(); @@ -79,8 +89,6 @@ public class PkiQbftExtraDataCodecTest { final Bytes vanity_data = Bytes.wrap(vanity_bytes); final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); - encoder.startList(); // start envelope list - encoder.startList(); // start extra data list // vanity data encoder.writeBytes(vanity_data); @@ -95,13 +103,54 @@ public class PkiQbftExtraDataCodecTest { encoder.writeIntScalar(round); // committer seals encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes())); + // cms + encoder.writeBytes(cms); encoder.endList(); // end extra data list - encoder.startList(); // start cms list - encoder.writeBytes(cms); - encoder.endList(); // end cms list + final Bytes bufferToInject = encoder.encoded(); - encoder.endList(); // end envelope list + final PkiQbftExtraData extraData = + (PkiQbftExtraData) bftExtraDataCodec.decodeRaw(bufferToInject); + + assertThat(extraData.getVanityData()).isEqualTo(vanity_data); + assertThat(extraData.getRound()).isEqualTo(round); + assertThat(extraData.getSeals()).isEqualTo(committerSeals); + assertThat(extraData.getValidators()).isEqualTo(validators); + assertThat(extraData.getCms()).isEqualTo(cms); + } + + @Test + public void decodingQbftExtraDataDelegatesToQbftCodec() { + final List
validators = + Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2")); + final int round = 0x00FEDCBA; + final List committerSeals = + Arrays.asList( + SIGNATURE_ALGORITHM.get().createSignature(BigInteger.ONE, BigInteger.TEN, (byte) 0), + SIGNATURE_ALGORITHM.get().createSignature(BigInteger.TEN, BigInteger.ONE, (byte) 0)); + + // Create randomised vanity data. + final byte[] vanity_bytes = createNonEmptyVanityData(); + new Random().nextBytes(vanity_bytes); + final Bytes vanity_data = Bytes.wrap(vanity_bytes); + + final BytesValueRLPOutput encoder = new BytesValueRLPOutput(); + encoder.startList(); // start extra data list + // vanity data + encoder.writeBytes(vanity_data); + // validators + encoder.writeList(validators, (validator, rlp) -> rlp.writeBytes(validator)); + // votes + encoder.startList(); + encoder.writeBytes(Address.fromHexString("1")); + encoder.writeByte(Vote.ADD_BYTE_VALUE); + encoder.endList(); + // rounds + encoder.writeIntScalar(round); + // committer seals + encoder.writeList(committerSeals, (committer, rlp) -> rlp.writeBytes(committer.encodedBytes())); + // Not including the CMS in the list (to generate a non-pki QBFT extra data) + encoder.endList(); // end extra data list final Bytes bufferToInject = encoder.encoded(); @@ -112,30 +161,70 @@ public class PkiQbftExtraDataCodecTest { assertThat(extraData.getRound()).isEqualTo(round); assertThat(extraData.getSeals()).isEqualTo(committerSeals); assertThat(extraData.getValidators()).isEqualTo(validators); - assertThat(extraData.getCms()).hasValue(cms); + assertThat(extraData.getCms()).isEqualTo(Bytes.EMPTY); + } + + /* + When encoding for blockchain, we ignore commit seals and round number, but we include the CMS + */ + @Test + public void encodingForBlockchainShouldIncludeCms() { + final Bytes expectedRawDecoding = + Bytes.fromHexString(RAW_EXCLUDE_COMMIT_SEALS_AND_ROUND_NUMBER_ENCODED_STRING); + final Bytes encoded = + bftExtraDataCodec.encodeWithoutCommitSealsAndRoundNumber(getDecodedExtraData(cms)); + + assertThat(encoded).isEqualTo(expectedRawDecoding); + } + + @Test + public void encodingWithoutCommitSealsShouldIncludeCms() { + final Bytes expectedRawDecoding = Bytes.fromHexString(RAW_EXCLUDE_COMMIT_SEALS_ENCODED_STRING); + final Bytes encoded = bftExtraDataCodec.encodeWithoutCommitSeals(getDecodedExtraData(cms)); + + assertThat(encoded).isEqualTo(expectedRawDecoding); } @Test - public void encodingExtraDataWithEmptyCmsMatchesKnownRawHexString() { - final Bytes expectedRawDecoding = Bytes.fromHexString(EMPTY_CMS_RAW_HEX_ENCODING_STRING); - final Bytes encoded = bftExtraDataCodec.encode(getDecodedExtraDataWithEmptyCms()); + public void encodingWithAllShouldIncludeCms() { + final Bytes expectedRawDecoding = Bytes.fromHexString(RAW_ALL_ENCODED_STRING); + final Bytes encoded = bftExtraDataCodec.encode(getDecodedExtraData(cms)); assertThat(encoded).isEqualTo(expectedRawDecoding); } + /* + When encoding for proposal, we include commit seals and round number, but we ignore the CMS + */ @Test - public void encodingMatchesKnownRawHexString() { - final Bytes expectedRawDecoding = Bytes.fromHexString(RAW_HEX_ENCODING_STRING); - final Bytes encoded = bftExtraDataCodec.encode(getDecodedExtraData(Optional.of(cms))); + public void encodingForCreatingCmsProposal() { + final Bytes expectedRawDecoding = Bytes.fromHexString(RAW_QBFT_EXTRA_DATA); + final Bytes encoded = bftExtraDataCodec.encodeWithoutCms(getDecodedExtraData(cms)); assertThat(encoded).isEqualTo(expectedRawDecoding); } - private static PkiQbftExtraData getDecodedExtraDataWithEmptyCms() { - return getDecodedExtraData(Optional.empty()); + /* + When encoding non-pki extra data, we delegate to the regular QBFT encoder + */ + @Test + public void encodingQbftExtraData() { + final Bytes expectedRawDecoding = Bytes.fromHexString(RAW_QBFT_EXTRA_DATA); + final PkiQbftExtraData pkiBftExtraData = getDecodedExtraData(cms); + final BftExtraData bftExtraData = + new BftExtraData( + pkiBftExtraData.getVanityData(), + pkiBftExtraData.getSeals(), + pkiBftExtraData.getVote(), + pkiBftExtraData.getRound(), + pkiBftExtraData.getValidators()); + + final Bytes encoded = bftExtraDataCodec.encode(bftExtraData); + + assertThat(encoded).isEqualTo(expectedRawDecoding); } - private static PkiQbftExtraData getDecodedExtraData(final Optional cms) { + private static PkiQbftExtraData getDecodedExtraData(final Bytes cms) { final List
validators = Arrays.asList(Address.fromHexString("1"), Address.fromHexString("2")); final Optional vote = Optional.of(Vote.authVote(Address.fromHexString("1"))); diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManagerTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManagerTest.java index 14c8e8eb42..00f99768b6 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManagerTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftBlockHeightManagerTest.java @@ -40,6 +40,7 @@ import org.hyperledger.besu.consensus.common.bft.blockcreation.BftBlockCreator; import org.hyperledger.besu.consensus.common.bft.events.RoundExpiry; import org.hyperledger.besu.consensus.common.bft.network.ValidatorMulticaster; import org.hyperledger.besu.consensus.common.bft.statemachine.BftFinalState; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.messagedata.RoundChangeMessageData; import org.hyperledger.besu.consensus.qbft.messagewrappers.Commit; @@ -148,7 +149,10 @@ public class QbftBlockHeightManagerTest { protocolContext = new ProtocolContext( - null, null, setupContextWithBftExtraDataEncoder(validators, new QbftExtraDataCodec())); + null, + null, + setupContextWithBftExtraDataEncoder( + QbftContext.class, validators, new QbftExtraDataCodec())); // Ensure the created IbftRound has the valid ConsensusRoundIdentifier; when(roundFactory.createNewRound(any(), anyInt())) diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundTest.java index 4bb36ffe9a..642db539d2 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/statemachine/QbftRoundTest.java @@ -36,6 +36,7 @@ import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.RoundTimer; import org.hyperledger.besu.consensus.common.bft.blockcreation.BftBlockCreator; import org.hyperledger.besu.consensus.common.bft.payload.SignedData; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.messagewrappers.RoundChange; import org.hyperledger.besu.consensus.qbft.network.QbftMessageTransmitter; @@ -109,7 +110,8 @@ public class QbftRoundTest { new ProtocolContext( blockChain, worldStateArchive, - setupContextWithBftExtraDataEncoder(emptyList(), new QbftExtraDataCodec())); + setupContextWithBftExtraDataEncoder( + QbftContext.class, emptyList(), new QbftExtraDataCodec())); when(messageValidator.validateProposal(any())).thenReturn(true); when(messageValidator.validatePrepare(any())).thenReturn(true); diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/ProposalPayloadValidatorTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/ProposalPayloadValidatorTest.java index 56c56ff7a5..ef7ab70fbc 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/ProposalPayloadValidatorTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/ProposalPayloadValidatorTest.java @@ -21,12 +21,18 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import org.hyperledger.besu.consensus.common.bft.BftBlockHeaderFunctions; +import org.hyperledger.besu.consensus.common.bft.BftExtraDataCodec; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundHelpers; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.ProposedBlockHelpers; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.messagewrappers.Proposal; import org.hyperledger.besu.consensus.qbft.payload.MessageFactory; +import org.hyperledger.besu.consensus.qbft.pki.PkiQbftBlockHeaderFunctions; +import org.hyperledger.besu.consensus.qbft.pki.PkiQbftExtraData; +import org.hyperledger.besu.consensus.qbft.pki.PkiQbftExtraDataCodec; import org.hyperledger.besu.crypto.NodeKey; import org.hyperledger.besu.crypto.NodeKeyUtils; import org.hyperledger.besu.ethereum.BlockValidator; @@ -35,12 +41,19 @@ import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.core.Address; import org.hyperledger.besu.ethereum.core.Block; +import org.hyperledger.besu.ethereum.core.BlockDataGenerator; +import org.hyperledger.besu.ethereum.core.BlockDataGenerator.BlockOptions; +import org.hyperledger.besu.ethereum.core.Hash; import org.hyperledger.besu.ethereum.core.Util; import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.pki.cms.CmsValidator; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import org.apache.tuweni.bytes.Bytes; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,6 +66,7 @@ public class ProposalPayloadValidatorTest { @Mock private BlockValidator blockValidator; @Mock private MutableBlockchain blockChain; @Mock private WorldStateArchive worldStateArchive; + @Mock private CmsValidator cmsValidator; private ProtocolContext protocolContext; private static final int CHAIN_HEIGHT = 3; @@ -64,7 +78,7 @@ public class ProposalPayloadValidatorTest { private final MessageFactory messageFactory = new MessageFactory(nodeKey); final ConsensusRoundIdentifier roundIdentifier = ConsensusRoundHelpers.createFrom(targetRound, 1, 0); - final QbftExtraDataCodec bftExtraDataEncoder = new QbftExtraDataCodec(); + final QbftExtraDataCodec bftExtraDataCodec = new QbftExtraDataCodec(); @Before public void setup() { @@ -72,16 +86,16 @@ public class ProposalPayloadValidatorTest { new ProtocolContext( blockChain, worldStateArchive, - setupContextWithBftExtraDataEncoder(emptyList(), bftExtraDataEncoder)); + setupContextWithBftExtraDataEncoder(QbftContext.class, emptyList(), bftExtraDataCodec)); } @Test public void validationPassesWhenProposerAndRoundMatchAndBlockIsValid() { final ProposalPayloadValidator payloadValidator = new ProposalPayloadValidator( - expectedProposer, roundIdentifier, blockValidator, protocolContext); + expectedProposer, roundIdentifier, blockValidator, protocolContext, bftExtraDataCodec); final Block block = - ProposedBlockHelpers.createProposalBlock(emptyList(), roundIdentifier, bftExtraDataEncoder); + ProposedBlockHelpers.createProposalBlock(emptyList(), roundIdentifier, bftExtraDataCodec); final Proposal proposal = messageFactory.createProposal(roundIdentifier, block, emptyList(), emptyList()); @@ -99,13 +113,13 @@ public class ProposalPayloadValidatorTest { public void validationPassesWhenBlockRoundDoesNotMatchProposalRound() { final ProposalPayloadValidator payloadValidator = new ProposalPayloadValidator( - expectedProposer, roundIdentifier, blockValidator, protocolContext); + expectedProposer, roundIdentifier, blockValidator, protocolContext, bftExtraDataCodec); final Block block = ProposedBlockHelpers.createProposalBlock( emptyList(), ConsensusRoundHelpers.createFrom(roundIdentifier, 0, +1), - bftExtraDataEncoder); + bftExtraDataCodec); final Proposal proposal = messageFactory.createProposal(roundIdentifier, block, emptyList(), emptyList()); @@ -126,9 +140,9 @@ public class ProposalPayloadValidatorTest { final ProposalPayloadValidator payloadValidator = new ProposalPayloadValidator( - expectedProposer, roundIdentifier, blockValidator, protocolContext); + expectedProposer, roundIdentifier, blockValidator, protocolContext, bftExtraDataCodec); final Block block = - ProposedBlockHelpers.createProposalBlock(emptyList(), roundIdentifier, bftExtraDataEncoder); + ProposedBlockHelpers.createProposalBlock(emptyList(), roundIdentifier, bftExtraDataCodec); final Proposal proposal = messageFactory.createProposal(roundIdentifier, block, emptyList(), emptyList()); @@ -146,7 +160,11 @@ public class ProposalPayloadValidatorTest { public void validationFailsWhenExpectedProposerDoesNotMatchPayloadsAuthor() { final ProposalPayloadValidator payloadValidator = new ProposalPayloadValidator( - Address.fromHexString("0x1"), roundIdentifier, blockValidator, protocolContext); + Address.fromHexString("0x1"), + roundIdentifier, + blockValidator, + protocolContext, + bftExtraDataCodec); final Block block = ProposedBlockHelpers.createProposalBlock(emptyList(), roundIdentifier); final Proposal proposal = messageFactory.createProposal(roundIdentifier, block, emptyList(), emptyList()); @@ -159,7 +177,7 @@ public class ProposalPayloadValidatorTest { public void validationFailsWhenMessageMismatchesExpectedRound() { final ProposalPayloadValidator payloadValidator = new ProposalPayloadValidator( - expectedProposer, roundIdentifier, blockValidator, protocolContext); + expectedProposer, roundIdentifier, blockValidator, protocolContext, bftExtraDataCodec); final Block block = ProposedBlockHelpers.createProposalBlock(emptyList(), roundIdentifier); final Proposal proposal = @@ -177,7 +195,7 @@ public class ProposalPayloadValidatorTest { public void validationFailsWhenMessageMismatchesExpectedHeight() { final ProposalPayloadValidator payloadValidator = new ProposalPayloadValidator( - expectedProposer, roundIdentifier, blockValidator, protocolContext); + expectedProposer, roundIdentifier, blockValidator, protocolContext, bftExtraDataCodec); final Block block = ProposedBlockHelpers.createProposalBlock(emptyList(), roundIdentifier); final Proposal proposal = @@ -195,12 +213,12 @@ public class ProposalPayloadValidatorTest { public void validationFailsForBlockWithIncorrectHeight() { final ProposalPayloadValidator payloadValidator = new ProposalPayloadValidator( - expectedProposer, roundIdentifier, blockValidator, protocolContext); + expectedProposer, roundIdentifier, blockValidator, protocolContext, bftExtraDataCodec); final Block block = ProposedBlockHelpers.createProposalBlock( emptyList(), ConsensusRoundHelpers.createFrom(roundIdentifier, +1, 0), - bftExtraDataEncoder); + bftExtraDataCodec); final Proposal proposal = messageFactory.createProposal(roundIdentifier, block, emptyList(), emptyList()); @@ -213,4 +231,102 @@ public class ProposalPayloadValidatorTest { assertThat(payloadValidator.validate(proposal.getSignedPayload())).isFalse(); } + + @Test + public void validationForCmsFailsWhenCmsFailsValidation() { + final PkiQbftExtraDataCodec pkiQbftExtraDataCodec = new PkiQbftExtraDataCodec(); + final QbftContext qbftContext = + setupContextWithBftExtraDataEncoder(QbftContext.class, emptyList(), pkiQbftExtraDataCodec); + final Bytes cms = Bytes.fromHexStringLenient("0x1"); + final ProtocolContext protocolContext = + new ProtocolContext(blockChain, worldStateArchive, qbftContext); + + final ProposalPayloadValidator payloadValidator = + new ProposalPayloadValidator( + expectedProposer, + roundIdentifier, + blockValidator, + protocolContext, + pkiQbftExtraDataCodec, + Optional.of(cmsValidator)); + final Block block = + createPkiProposalBlock(emptyList(), roundIdentifier, pkiQbftExtraDataCodec, cms); + final Proposal proposal = + messageFactory.createProposal(roundIdentifier, block, emptyList(), emptyList()); + final Hash hashWithoutCms = + PkiQbftBlockHeaderFunctions.forCmsSignature(pkiQbftExtraDataCodec).hash(block.getHeader()); + + when(blockValidator.validateAndProcessBlock( + eq(protocolContext), + eq(block), + eq(HeaderValidationMode.LIGHT), + eq(HeaderValidationMode.FULL))) + .thenReturn(Optional.of(new BlockProcessingOutputs(null, null))); + when(cmsValidator.validate(eq(cms), eq(hashWithoutCms))).thenReturn(false); + + assertThat(payloadValidator.validate(proposal.getSignedPayload())).isFalse(); + } + + @Test + public void validationForCmsPassesWhenCmsIsValid() { + final PkiQbftExtraDataCodec pkiQbftExtraDataCodec = new PkiQbftExtraDataCodec(); + final QbftContext qbftContext = + setupContextWithBftExtraDataEncoder(QbftContext.class, emptyList(), pkiQbftExtraDataCodec); + final Bytes cms = Bytes.fromHexStringLenient("0x1"); + final ProtocolContext protocolContext = + new ProtocolContext(blockChain, worldStateArchive, qbftContext); + + final ProposalPayloadValidator payloadValidator = + new ProposalPayloadValidator( + expectedProposer, + roundIdentifier, + blockValidator, + protocolContext, + pkiQbftExtraDataCodec, + Optional.of(cmsValidator)); + final Block block = + createPkiProposalBlock(emptyList(), roundIdentifier, pkiQbftExtraDataCodec, cms); + final Proposal proposal = + messageFactory.createProposal(roundIdentifier, block, emptyList(), emptyList()); + final Hash hashWithoutCms = + PkiQbftBlockHeaderFunctions.forCmsSignature(pkiQbftExtraDataCodec).hash(block.getHeader()); + + when(blockValidator.validateAndProcessBlock( + eq(protocolContext), + eq(block), + eq(HeaderValidationMode.LIGHT), + eq(HeaderValidationMode.FULL))) + .thenReturn(Optional.of(new BlockProcessingOutputs(null, null))); + when(cmsValidator.validate(eq(cms), eq(hashWithoutCms))).thenReturn(true); + + assertThat(payloadValidator.validate(proposal.getSignedPayload())).isTrue(); + } + + public static Block createPkiProposalBlock( + final List
validators, + final ConsensusRoundIdentifier roundId, + final BftExtraDataCodec bftExtraDataCodec, + final Bytes cms) { + final Bytes extraData = + bftExtraDataCodec.encode( + new PkiQbftExtraData( + Bytes.wrap(new byte[32]), + Collections.emptyList(), + Optional.empty(), + roundId.getRoundNumber(), + validators, + cms)); + final BlockOptions blockOptions = + BlockOptions.create() + .setExtraData(extraData) + .setBlockNumber(roundId.getSequenceNumber()) + .setBlockHeaderFunctions(BftBlockHeaderFunctions.forCommittedSeal(bftExtraDataCodec)) + .hasOmmers(false) + .hasTransactions(false); + + if (validators.size() > 0) { + blockOptions.setCoinbase(validators.get(0)); + } + return new BlockDataGenerator().block(blockOptions); + } } diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/ProposalValidatorTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/ProposalValidatorTest.java index a5434a6ff1..e4f1551592 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/ProposalValidatorTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/ProposalValidatorTest.java @@ -28,6 +28,7 @@ import org.hyperledger.besu.consensus.common.bft.ConsensusRoundHelpers; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.ProposedBlockHelpers; import org.hyperledger.besu.consensus.common.bft.payload.SignedData; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.messagewrappers.Prepare; import org.hyperledger.besu.consensus.qbft.messagewrappers.Proposal; @@ -95,7 +96,8 @@ public class ProposalValidatorTest { new ProtocolContext( blockChain, worldStateArchive, - setupContextWithBftExtraDataEncoder(emptyList(), bftExtraDataEncoder)); + setupContextWithBftExtraDataEncoder( + QbftContext.class, emptyList(), bftExtraDataEncoder)); // typically tests require the blockValidation to be successful when(blockValidator.validateAndProcessBlock( diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/RoundChangeMessageValidatorTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/RoundChangeMessageValidatorTest.java index 54728e1855..545e8b5727 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/RoundChangeMessageValidatorTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/validation/RoundChangeMessageValidatorTest.java @@ -29,6 +29,7 @@ import org.hyperledger.besu.consensus.common.bft.ConsensusRoundHelpers; import org.hyperledger.besu.consensus.common.bft.ConsensusRoundIdentifier; import org.hyperledger.besu.consensus.common.bft.ProposedBlockHelpers; import org.hyperledger.besu.consensus.common.bft.payload.SignedData; +import org.hyperledger.besu.consensus.qbft.QbftContext; import org.hyperledger.besu.consensus.qbft.QbftExtraDataCodec; import org.hyperledger.besu.consensus.qbft.messagewrappers.RoundChange; import org.hyperledger.besu.consensus.qbft.payload.PreparedRoundMetadata; @@ -79,7 +80,8 @@ public class RoundChangeMessageValidatorTest { new ProtocolContext( blockChain, worldStateArchive, - setupContextWithBftExtraDataEncoder(emptyList(), bftExtraDataEncoder)); + setupContextWithBftExtraDataEncoder( + QbftContext.class, emptyList(), bftExtraDataEncoder)); } @Test diff --git a/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsValidator.java b/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsValidator.java index a260c40478..8d36fbc15c 100644 --- a/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsValidator.java +++ b/pki/src/main/java/org/hyperledger/besu/pki/cms/CmsValidator.java @@ -70,6 +70,10 @@ public class CmsValidator { * is trusted, otherwise returns false. */ public boolean validate(final Bytes cms, final Bytes expectedContent) { + if (cms == null || cms == Bytes.EMPTY) { + return false; + } + try { LOGGER.trace("Validating CMS message"); diff --git a/pki/src/test/java/org/hyperledger/besu/pki/cms/CmsCreationAndValidationTest.java b/pki/src/test/java/org/hyperledger/besu/pki/cms/CmsCreationAndValidationTest.java index 6a82eb3d87..72628d39c4 100644 --- a/pki/src/test/java/org/hyperledger/besu/pki/cms/CmsCreationAndValidationTest.java +++ b/pki/src/test/java/org/hyperledger/besu/pki/cms/CmsCreationAndValidationTest.java @@ -220,6 +220,13 @@ public class CmsCreationAndValidationTest { cmsValidator = new CmsValidator(truststoreWrapper); } + @Test + public void cmsValidationWithEmptyCmsMessage() { + final Bytes data = Bytes.random(32); + + assertThat(cmsValidator.validate(Bytes.EMPTY, data)).isFalse(); + } + @Test public void cmsValidationWithTrustedSelfSignedCertificate() { final CmsCreator cmsCreator = new CmsCreator(keystoreWrapper, "trusted_selfsigned");