Skip to content

Commit 812dc74

Browse files
authored
Add evmtool block-test subcommand (#7293)
* Add evmtool block-test subcommand Add an evmtool subcommand that will run non-hive blockchain tests. Signed-off-by: Danno Ferrin <danno@numisight.com>
1 parent 3f00bad commit 812dc74

File tree

6 files changed

+531
-10
lines changed

6 files changed

+531
-10
lines changed

CHANGELOG.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
### Breaking Changes
66

77
### Additions and Improvements
8+
- `--Xsnapsync-bft-enabled` option enables experimental support for snap sync with IBFT/QBFT permissioned Bonsai-DB chains [#7140](https://github.com/hyperledger/besu/pull/7140)
89
- Add support to load external profiles using `--profile` [#7265](https://github.com/hyperledger/besu/issues/7265)
10+
- `privacy-nonce-always-increments` option enables private transactions to always increment the nonce, even if the transaction is invalid [#6593](https://github.com/hyperledger/besu/pull/6593)
11+
- Added `block-test` subcommand to the evmtool which runs blockchain reference tests [#7293](https://github.com/hyperledger/besu/pull/7293)
912

1013
### Bug fixes
1114

@@ -34,8 +37,6 @@
3437
- Nodes in a permissioned chain maintain (and retry) connections to bootnodes [#7257](https://github.com/hyperledger/besu/pull/7257)
3538
- Promote experimental `besu storage x-trie-log` subcommand to production-ready [#7278](https://github.com/hyperledger/besu/pull/7278)
3639
- Enhanced BFT round-change diagnostics [#7271](https://github.com/hyperledger/besu/pull/7271)
37-
- `--Xsnapsync-bft-enabled` option enables experimental support for snap sync with IBFT/QBFT permissioned Bonsai-DB chains [#7140](https://github.com/hyperledger/besu/pull/7140)
38-
- `privacy-nonce-always-increments` option enables private transactions to always increment the nonce, even if the transaction is invalid [#6593](https://github.com/hyperledger/besu/pull/6593)
3940

4041
### Bug fixes
4142
- Validation errors ignored in accounts-allowlist and empty list [#7138](https://github.com/hyperledger/besu/issues/7138)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
* Copyright ConsenSys AG.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.evmtool;
16+
17+
import static java.nio.charset.StandardCharsets.UTF_8;
18+
import static org.hyperledger.besu.evmtool.BlockchainTestSubCommand.COMMAND_NAME;
19+
20+
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
21+
import org.hyperledger.besu.ethereum.ProtocolContext;
22+
import org.hyperledger.besu.ethereum.chain.MutableBlockchain;
23+
import org.hyperledger.besu.ethereum.core.Block;
24+
import org.hyperledger.besu.ethereum.core.BlockHeader;
25+
import org.hyperledger.besu.ethereum.core.BlockImporter;
26+
import org.hyperledger.besu.ethereum.core.MutableWorldState;
27+
import org.hyperledger.besu.ethereum.mainnet.BlockImportResult;
28+
import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode;
29+
import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule;
30+
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
31+
import org.hyperledger.besu.ethereum.referencetests.BlockchainReferenceTestCaseSpec;
32+
import org.hyperledger.besu.ethereum.referencetests.ReferenceTestProtocolSchedules;
33+
import org.hyperledger.besu.ethereum.rlp.RLPException;
34+
import org.hyperledger.besu.evm.EVM;
35+
import org.hyperledger.besu.evm.EvmSpecVersion;
36+
import org.hyperledger.besu.evm.account.AccountState;
37+
import org.hyperledger.besu.evm.internal.EvmConfiguration.WorldUpdaterMode;
38+
39+
import java.io.BufferedReader;
40+
import java.io.File;
41+
import java.io.IOException;
42+
import java.io.InputStreamReader;
43+
import java.nio.file.Path;
44+
import java.util.ArrayList;
45+
import java.util.List;
46+
import java.util.Map;
47+
import java.util.function.Supplier;
48+
49+
import com.fasterxml.jackson.core.JsonProcessingException;
50+
import com.fasterxml.jackson.databind.JavaType;
51+
import com.fasterxml.jackson.databind.ObjectMapper;
52+
import com.google.common.base.Suppliers;
53+
import org.apache.tuweni.bytes.Bytes32;
54+
import picocli.CommandLine.Command;
55+
import picocli.CommandLine.Option;
56+
import picocli.CommandLine.Parameters;
57+
import picocli.CommandLine.ParentCommand;
58+
59+
/**
60+
* This class, BlockchainTestSubCommand, is a command-line interface (CLI) command that executes an
61+
* Ethereum State Test. It implements the Runnable interface, meaning it can be used in a thread of
62+
* execution.
63+
*
64+
* <p>The class is annotated with @CommandLine.Command, which is a PicoCLI annotation that
65+
* designates this class as a command-line command. The annotation parameters define the command's
66+
* name, description, whether it includes standard help options, and the version provider.
67+
*
68+
* <p>The command's functionality is defined in the run() method, which is overridden from the
69+
* Runnable interface.
70+
*/
71+
@Command(
72+
name = COMMAND_NAME,
73+
description = "Execute an Ethereum Blockchain Test.",
74+
mixinStandardHelpOptions = true,
75+
versionProvider = VersionProvider.class)
76+
public class BlockchainTestSubCommand implements Runnable {
77+
/**
78+
* The name of the command for the BlockchainTestSubCommand. This constant is used as the name
79+
* parameter in the @CommandLine.Command annotation. It defines the command name that users should
80+
* enter on the command line to invoke this command.
81+
*/
82+
public static final String COMMAND_NAME = "block-test";
83+
84+
static final Supplier<ReferenceTestProtocolSchedules> referenceTestProtocolSchedules =
85+
Suppliers.memoize(ReferenceTestProtocolSchedules::create);
86+
87+
@Option(
88+
names = {"--test-name"},
89+
description = "Limit execution to one named test.")
90+
private String testName = null;
91+
92+
@ParentCommand private final EvmToolCommand parentCommand;
93+
94+
// picocli does it magically
95+
@Parameters private final List<Path> blockchainTestFiles = new ArrayList<>();
96+
97+
/**
98+
* Default constructor for the BlockchainTestSubCommand class. This constructor doesn't take any
99+
* arguments and initializes the parentCommand to null. PicoCLI requires this constructor.
100+
*/
101+
@SuppressWarnings("unused")
102+
public BlockchainTestSubCommand() {
103+
// PicoCLI requires this
104+
this(null);
105+
}
106+
107+
BlockchainTestSubCommand(final EvmToolCommand parentCommand) {
108+
this.parentCommand = parentCommand;
109+
}
110+
111+
@Override
112+
public void run() {
113+
// presume ethereum mainnet for reference and state tests
114+
SignatureAlgorithmFactory.setDefaultInstance();
115+
final ObjectMapper blockchainTestMapper = JsonUtils.createObjectMapper();
116+
117+
final JavaType javaType =
118+
blockchainTestMapper
119+
.getTypeFactory()
120+
.constructParametricType(
121+
Map.class, String.class, BlockchainReferenceTestCaseSpec.class);
122+
try {
123+
if (blockchainTestFiles.isEmpty()) {
124+
// if no state tests were specified, use standard input to get filenames
125+
final BufferedReader in =
126+
new BufferedReader(new InputStreamReader(parentCommand.in, UTF_8));
127+
while (true) {
128+
final String fileName = in.readLine();
129+
if (fileName == null) {
130+
// Reached end-of-file. Stop the loop.
131+
break;
132+
}
133+
final File file = new File(fileName);
134+
if (file.isFile()) {
135+
final Map<String, BlockchainReferenceTestCaseSpec> blockchainTests =
136+
blockchainTestMapper.readValue(file, javaType);
137+
executeBlockchainTest(blockchainTests);
138+
} else {
139+
parentCommand.out.println("File not found: " + fileName);
140+
}
141+
}
142+
} else {
143+
for (final Path blockchainTestFile : blockchainTestFiles) {
144+
final Map<String, BlockchainReferenceTestCaseSpec> blockchainTests;
145+
if ("stdin".equals(blockchainTestFile.toString())) {
146+
blockchainTests = blockchainTestMapper.readValue(parentCommand.in, javaType);
147+
} else {
148+
blockchainTests = blockchainTestMapper.readValue(blockchainTestFile.toFile(), javaType);
149+
}
150+
executeBlockchainTest(blockchainTests);
151+
}
152+
}
153+
} catch (final JsonProcessingException jpe) {
154+
parentCommand.out.println("File content error: " + jpe);
155+
} catch (final IOException e) {
156+
System.err.println("Unable to read state file");
157+
e.printStackTrace(System.err);
158+
}
159+
}
160+
161+
private void executeBlockchainTest(
162+
final Map<String, BlockchainReferenceTestCaseSpec> blockchainTests) {
163+
blockchainTests.forEach(this::traceTestSpecs);
164+
}
165+
166+
private void traceTestSpecs(final String test, final BlockchainReferenceTestCaseSpec spec) {
167+
if (testName != null && !testName.equals(test)) {
168+
parentCommand.out.println("Skipping test: " + test);
169+
return;
170+
}
171+
parentCommand.out.println("Considering " + test);
172+
173+
final BlockHeader genesisBlockHeader = spec.getGenesisBlockHeader();
174+
final MutableWorldState worldState =
175+
spec.getWorldStateArchive()
176+
.getMutable(genesisBlockHeader.getStateRoot(), genesisBlockHeader.getHash())
177+
.orElseThrow();
178+
179+
final ProtocolSchedule schedule =
180+
referenceTestProtocolSchedules.get().getByName(spec.getNetwork());
181+
182+
final MutableBlockchain blockchain = spec.getBlockchain();
183+
final ProtocolContext context = spec.getProtocolContext();
184+
185+
for (final BlockchainReferenceTestCaseSpec.CandidateBlock candidateBlock :
186+
spec.getCandidateBlocks()) {
187+
if (!candidateBlock.isExecutable()) {
188+
return;
189+
}
190+
191+
try {
192+
final Block block = candidateBlock.getBlock();
193+
194+
final ProtocolSpec protocolSpec = schedule.getByBlockHeader(block.getHeader());
195+
final BlockImporter blockImporter = protocolSpec.getBlockImporter();
196+
197+
verifyJournaledEVMAccountCompatability(worldState, protocolSpec);
198+
199+
final HeaderValidationMode validationMode =
200+
"NoProof".equalsIgnoreCase(spec.getSealEngine())
201+
? HeaderValidationMode.LIGHT
202+
: HeaderValidationMode.FULL;
203+
final BlockImportResult importResult =
204+
blockImporter.importBlock(context, block, validationMode, validationMode);
205+
206+
if (importResult.isImported() != candidateBlock.isValid()) {
207+
parentCommand.out.printf(
208+
"Block %d (%s) %s%n",
209+
block.getHeader().getNumber(),
210+
block.getHash(),
211+
importResult.isImported() ? "Failed to be rejected" : "Failed to import");
212+
} else {
213+
parentCommand.out.printf(
214+
"Block %d (%s) %s%n",
215+
block.getHeader().getNumber(),
216+
block.getHash(),
217+
importResult.isImported() ? "Imported" : "Rejected (correctly)");
218+
}
219+
} catch (final RLPException e) {
220+
if (candidateBlock.isValid()) {
221+
parentCommand.out.printf(
222+
"Block %d (%s) should have imported but had an RLP exception %s%n",
223+
candidateBlock.getBlock().getHeader().getNumber(),
224+
candidateBlock.getBlock().getHash(),
225+
e.getMessage());
226+
}
227+
}
228+
}
229+
if (!blockchain.getChainHeadHash().equals(spec.getLastBlockHash())) {
230+
parentCommand.out.printf(
231+
"Chain header mismatch, have %s want %s - %s%n",
232+
blockchain.getChainHeadHash(), spec.getLastBlockHash(), test);
233+
} else {
234+
parentCommand.out.println("Chain import successful - " + test);
235+
}
236+
}
237+
238+
void verifyJournaledEVMAccountCompatability(
239+
final MutableWorldState worldState, final ProtocolSpec protocolSpec) {
240+
EVM evm = protocolSpec.getEvm();
241+
if (evm.getEvmConfiguration().worldUpdaterMode() == WorldUpdaterMode.JOURNALED) {
242+
if (worldState
243+
.streamAccounts(Bytes32.ZERO, Integer.MAX_VALUE)
244+
.anyMatch(AccountState::isEmpty)) {
245+
parentCommand.out.println("Journaled account configured and empty account detected");
246+
}
247+
248+
if (EvmSpecVersion.SPURIOUS_DRAGON.compareTo(evm.getEvmVersion()) > 0) {
249+
parentCommand.out.println(
250+
"Journaled account configured and fork prior to the merge specified");
251+
}
252+
}
253+
}
254+
}

ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommand.java

+10-6
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
subcommands = {
102102
BenchmarkSubCommand.class,
103103
B11rSubCommand.class,
104+
BlockchainTestSubCommand.class,
104105
CodeValidateSubCommand.class,
105106
EOFTestSubCommand.class,
106107
PrettyPrintSubCommand.class,
@@ -370,15 +371,18 @@ public boolean hasFork() {
370371
public void run() {
371372
LogConfigurator.setLevel("", "OFF");
372373
try {
374+
GenesisFileModule genesisFileModule;
375+
if (network != null) {
376+
genesisFileModule = GenesisFileModule.createGenesisModule(network);
377+
} else if (genesisFile != null) {
378+
genesisFileModule = GenesisFileModule.createGenesisModule(genesisFile);
379+
} else {
380+
genesisFileModule = GenesisFileModule.createGenesisModule(NetworkName.DEV);
381+
}
373382
final EvmToolComponent component =
374383
DaggerEvmToolComponent.builder()
375384
.dataStoreModule(new DataStoreModule())
376-
.genesisFileModule(
377-
network == null
378-
? genesisFile == null
379-
? GenesisFileModule.createGenesisModule(NetworkName.DEV)
380-
: GenesisFileModule.createGenesisModule(genesisFile)
381-
: GenesisFileModule.createGenesisModule(network))
385+
.genesisFileModule(genesisFileModule)
382386
.evmToolCommandOptionsModule(daggerOptions)
383387
.metricsSystemModule(new MetricsSystemModule())
384388
.build();

ethereum/evmtool/src/test/java/org/hyperledger/besu/evmtool/EvmToolSpecTests.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ public class EvmToolSpecTests {
5353
static final ObjectMapper objectMapper = new ObjectMapper();
5454
static final ObjectReader specReader = objectMapper.reader();
5555

56+
public static Object[][] blocktestTests() {
57+
return findSpecFiles(new String[] {"block-test"});
58+
}
59+
5660
public static Object[][] b11rTests() {
5761
return findSpecFiles(new String[] {"b11r"});
5862
}
@@ -114,7 +118,14 @@ private static Object[] pathToParams(final String subDir, final File file) {
114118
}
115119

116120
@ParameterizedTest(name = "{0}")
117-
@MethodSource({"b11rTests", "prettyPrintTests", "stateTestTests", "t8nTests", "traceTests"})
121+
@MethodSource({
122+
"blocktestTests",
123+
"b11rTests",
124+
"prettyPrintTests",
125+
"stateTestTests",
126+
"t8nTests",
127+
"traceTests"
128+
})
118129
void testBySpec(
119130
final String file,
120131
final JsonNode cliNode,

0 commit comments

Comments
 (0)