EvmToolCommand.java
/*
* 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.evmtool;
import static java.nio.charset.StandardCharsets.UTF_8;
import static picocli.CommandLine.ScopeType.INHERIT;
import org.hyperledger.besu.cli.config.NetworkName;
import org.hyperledger.besu.collections.trie.BytesTrieSet;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.BlockHeaderBuilder;
import org.hyperledger.besu.ethereum.core.Difficulty;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions;
import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec;
import org.hyperledger.besu.ethereum.vm.CachingBlockHashLookup;
import org.hyperledger.besu.evm.Code;
import org.hyperledger.besu.evm.EVM;
import org.hyperledger.besu.evm.EvmSpecVersion;
import org.hyperledger.besu.evm.account.AccountStorageEntry;
import org.hyperledger.besu.evm.code.CodeInvalid;
import org.hyperledger.besu.evm.frame.MessageFrame;
import org.hyperledger.besu.evm.log.LogsBloomFilter;
import org.hyperledger.besu.evm.tracing.OperationTracer;
import org.hyperledger.besu.evm.tracing.StandardJsonTracer;
import org.hyperledger.besu.evm.worldstate.WorldState;
import org.hyperledger.besu.evm.worldstate.WorldUpdater;
import org.hyperledger.besu.metrics.MetricsSystemModule;
import org.hyperledger.besu.util.LogConfigurator;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.NavigableMap;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import io.vertx.core.json.JsonObject;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.units.bigints.UInt256;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
@Command(
description = "This command evaluates EVM transactions.",
abbreviateSynopsis = true,
name = "evm",
mixinStandardHelpOptions = true,
versionProvider = VersionProvider.class,
sortOptions = false,
header = "Usage:",
synopsisHeading = "%n",
descriptionHeading = "%nDescription:%n%n",
optionListHeading = "%nOptions:%n",
footerHeading = "%n",
footer = "Hyperledger Besu is licensed under the Apache License 2.0",
subcommands = {
BenchmarkSubCommand.class,
B11rSubCommand.class,
CodeValidateSubCommand.class,
StateTestSubCommand.class,
T8nSubCommand.class,
T8nServerSubCommand.class
})
public class EvmToolCommand implements Runnable {
@Option(
names = {"--code"},
paramLabel = "<code>",
description = "Byte stream of code to be executed.")
void setBytes(final String optionValue) {
codeBytes = Bytes.fromHexString(optionValue.replace(" ", ""));
}
private Bytes codeBytes = Bytes.EMPTY;
@Option(
names = {"--gas"},
description = "Amount of gas for this invocation.",
paramLabel = "<int>")
private final Long gas = 10_000_000_000L;
@Option(
names = {"--intrinsic-gas"},
description = "Calculate and charge intrinsic and tx content gas. Default is not to charge.",
scope = INHERIT,
negatable = true)
final Boolean chargeIntrinsicGas = false;
@Option(
names = {"--price"},
description = "Price of gas (in GWei) for this invocation",
paramLabel = "<int>")
private final Wei gasPriceGWei = Wei.ZERO;
@Option(
names = {"--blob-price"},
description = "Price of blob gas for this invocation",
paramLabel = "<int>")
private final Wei blobGasPrice = Wei.ZERO;
@Option(
names = {"--sender"},
paramLabel = "<address>",
description = "Calling address for this invocation.")
private final Address sender = Address.ZERO;
@Option(
names = {"--receiver"},
paramLabel = "<address>",
description = "Receiving address for this invocation.")
private final Address receiver = Address.ZERO;
@Option(
names = {"--contract"},
paramLabel = "<address>",
description = "The address holding the contract code.")
private final Address contract = Address.ZERO;
@Option(
names = {"--coinbase"},
paramLabel = "<address>",
description = "Coinbase for this invocation.")
private final Address coinbase = Address.ZERO;
@Option(
names = {"--input"},
paramLabel = "<code>",
description = "The CALLDATA for this invocation")
private final Bytes callData = Bytes.EMPTY;
@Option(
names = {"--value"},
description = "The amount of ether attached to this invocation",
paramLabel = "<int>")
private final Wei ethValue = Wei.ZERO;
@Option(
names = {"--json", "--trace"},
description = "Trace each opcode as a json object.",
scope = INHERIT)
final Boolean showJsonResults = false;
@Option(
names = {"--json-alloc"},
description = "Output the final allocations after a run.",
scope = INHERIT)
final Boolean showJsonAlloc = false;
@Option(
names = {"--memory", "--trace.memory"},
description =
"Show the full memory output in tracing for each op. Default is not to show memory.",
scope = INHERIT,
negatable = true)
final Boolean showMemory = false;
@Option(
names = {"--trace.nostack"},
description = "Show the operand stack in tracing for each op. Default is to show stack.",
scope = INHERIT,
negatable = true)
final Boolean hideStack = false;
@Option(
names = {"--trace.returndata"},
description =
"Show the return data in tracing for each op when present. Default is to show return data.",
scope = INHERIT,
negatable = true)
final Boolean showReturnData = false;
@Option(
names = {"--trace.storage"},
description =
"Show the updated storage slots for the current account. Default is to not show updated storage.",
scope = INHERIT,
negatable = true)
final Boolean showStorage = false;
@Option(
names = {"--notime"},
description = "Don't include time data in summary output.",
scope = INHERIT,
negatable = true)
final Boolean noTime = false;
@Option(
names = {"--prestate", "--genesis"},
description = "The genesis file containing account data for this invocation.")
private final File genesisFile = null;
@Option(
names = {"--chain"},
description = "Name of a well known network that will be used for this invocation.")
private final NetworkName network = null;
@Option(
names = {"--repeat"},
description = "Number of times to repeat for benchmarking.")
private final Integer repeat = 0;
@Option(
names = {"-v", "--version"},
versionHelp = true,
description = "display version info")
boolean versionInfoRequested;
static final Joiner STORAGE_JOINER = Joiner.on(",\n");
private final EvmToolCommandOptionsModule daggerOptions = new EvmToolCommandOptionsModule();
PrintWriter out;
InputStream in;
public EvmToolCommand() {
this(
new ByteArrayInputStream(new byte[0]),
new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8)), true));
}
public EvmToolCommand(final InputStream in, final PrintWriter out) {
this.in = in;
this.out = out;
}
void execute(final String... args) {
execute(System.in, new PrintWriter(System.out, true, UTF_8), args);
}
void execute(final InputStream input, final PrintWriter output, final String[] args) {
final CommandLine commandLine = new CommandLine(this).setOut(output);
out = output;
in = input;
// don't require exact case to match enum values
commandLine.setCaseInsensitiveEnumValuesAllowed(true);
// add dagger-injected options
commandLine.addMixin("Dagger Options", daggerOptions);
// add sub commands here
commandLine.registerConverter(Address.class, Address::fromHexString);
commandLine.registerConverter(Bytes.class, Bytes::fromHexString);
commandLine.registerConverter(Wei.class, arg -> Wei.of(Long.parseUnsignedLong(arg)));
// change negation regexp so --nomemory works. See
// https://picocli.info/#_customizing_negatable_options
commandLine.setNegatableOptionTransformer(
new CommandLine.RegexTransformer.Builder()
.addPattern("^--no(\\w(-|\\w)*)$", "--$1", "--[no]$1")
.addPattern("^--trace.no(\\w(-|\\w)*)$", "--trace.$1", "--trace.[no]$1")
.addPattern("^--(\\w(-|\\w)*)$", "--no$1", "--[no]$1")
.addPattern("^--trace.(\\w(-|\\w)*)$", "--trace.no$1", "--trace.[no]$1")
.build());
// Enumerate forks to support execution-spec-tests
addForkHelp(commandLine.getSubcommands().get("t8n"));
addForkHelp(commandLine.getSubcommands().get("t8n-server"));
commandLine.setExecutionStrategy(new CommandLine.RunLast());
commandLine.execute(args);
}
private static void addForkHelp(final CommandLine subCommandLine) {
subCommandLine
.getHelpSectionMap()
.put("forks_header", help -> help.createHeading("%nKnown Forks:%n"));
subCommandLine
.getHelpSectionMap()
.put(
"forks",
help ->
help.createTextTable(
Arrays.stream(EvmSpecVersion.values())
.collect(
Collectors.toMap(
EvmSpecVersion::getName,
EvmSpecVersion::getDescription,
(a, b) -> b,
LinkedHashMap::new)))
.toString());
List<String> keys = new ArrayList<>(subCommandLine.getHelpSectionKeys());
int index = keys.indexOf(CommandLine.Model.UsageMessageSpec.SECTION_KEY_FOOTER_HEADING);
keys.add(index, "forks_header");
keys.add(index + 1, "forks");
subCommandLine.setHelpSectionKeys(keys);
}
@Override
public void run() {
LogConfigurator.setLevel("", "OFF");
try {
final EvmToolComponent component =
DaggerEvmToolComponent.builder()
.dataStoreModule(new DataStoreModule())
.genesisFileModule(
network == null
? genesisFile == null
? GenesisFileModule.createGenesisModule(NetworkName.DEV)
: GenesisFileModule.createGenesisModule(genesisFile)
: GenesisFileModule.createGenesisModule(network))
.evmToolCommandOptionsModule(daggerOptions)
.metricsSystemModule(new MetricsSystemModule())
.build();
int remainingIters = this.repeat;
final ProtocolSpec protocolSpec =
component.getProtocolSpec().apply(BlockHeaderBuilder.createDefault().buildBlockHeader());
final Transaction tx =
new Transaction.Builder()
.nonce(0)
.gasPrice(Wei.ZERO)
.gasLimit(Long.MAX_VALUE)
.to(receiver)
.value(Wei.ZERO)
.payload(callData)
.sender(sender)
.build();
long txGas = gas;
if (chargeIntrinsicGas) {
final long intrinsicGasCost =
protocolSpec
.getGasCalculator()
.transactionIntrinsicGasCost(tx.getPayload(), tx.isContractCreation());
txGas -= intrinsicGasCost;
final long accessListCost =
tx.getAccessList()
.map(list -> protocolSpec.getGasCalculator().accessListGasCost(list))
.orElse(0L);
txGas -= accessListCost;
}
final EVM evm = protocolSpec.getEvm();
if (codeBytes.isEmpty()) {
codeBytes = component.getWorldState().get(receiver).getCode();
}
Code code = evm.getCode(Hash.hash(codeBytes), codeBytes);
if (!code.isValid()) {
out.println(((CodeInvalid) code).getInvalidReason());
return;
}
final Stopwatch stopwatch = Stopwatch.createUnstarted();
long lastTime = 0;
do {
final boolean lastLoop = remainingIters == 0;
final OperationTracer tracer = // You should have picked Mercy.
lastLoop && showJsonResults
? new StandardJsonTracer(out, showMemory, !hideStack, showReturnData, showStorage)
: OperationTracer.NO_TRACING;
WorldUpdater updater = component.getWorldUpdater();
updater.getOrCreate(sender);
updater.getOrCreate(receiver);
var contractAccount = updater.getOrCreate(contract);
contractAccount.setCode(codeBytes);
final Set<Address> addressList = new BytesTrieSet<>(Address.SIZE);
addressList.add(sender);
addressList.add(contract);
if (EvmSpecVersion.SHANGHAI.compareTo(evm.getEvmVersion()) <= 0) {
addressList.add(coinbase);
}
final BlockHeader blockHeader =
BlockHeaderBuilder.create()
.parentHash(Hash.EMPTY)
.coinbase(coinbase)
.difficulty(Difficulty.ONE)
.number(1)
.gasLimit(5000)
.timestamp(Instant.now().toEpochMilli())
.ommersHash(Hash.EMPTY_LIST_HASH)
.stateRoot(Hash.EMPTY_TRIE_HASH)
.transactionsRoot(Hash.EMPTY)
.receiptsRoot(Hash.EMPTY)
.logsBloom(LogsBloomFilter.empty())
.gasUsed(0)
.extraData(Bytes.EMPTY)
.mixHash(Hash.EMPTY)
.nonce(0)
.blockHeaderFunctions(new MainnetBlockHeaderFunctions())
.baseFee(component.getBlockchain().getChainHeadHeader().getBaseFee().orElse(null))
.buildBlockHeader();
MessageFrame initialMessageFrame =
MessageFrame.builder()
.type(MessageFrame.Type.MESSAGE_CALL)
.worldUpdater(updater.updater())
.initialGas(txGas)
.contract(Address.ZERO)
.address(receiver)
.originator(sender)
.sender(sender)
.gasPrice(gasPriceGWei)
.blobGasPrice(blobGasPrice)
.inputData(callData)
.value(ethValue)
.apparentValue(ethValue)
.code(code)
.blockValues(blockHeader)
.completer(c -> {})
.miningBeneficiary(blockHeader.getCoinbase())
.blockHashLookup(new CachingBlockHashLookup(blockHeader, component.getBlockchain()))
.accessListWarmAddresses(addressList)
.build();
Deque<MessageFrame> messageFrameStack = initialMessageFrame.getMessageFrameStack();
stopwatch.start();
while (!messageFrameStack.isEmpty()) {
final MessageFrame messageFrame = messageFrameStack.peek();
protocolSpec.getTransactionProcessor().process(messageFrame, tracer);
if (messageFrameStack.isEmpty()) {
stopwatch.stop();
if (lastTime == 0) {
lastTime = stopwatch.elapsed().toNanos();
}
if (lastLoop) {
if (messageFrame.getExceptionalHaltReason().isPresent()) {
out.println(messageFrame.getExceptionalHaltReason().get());
}
if (messageFrame.getRevertReason().isPresent()) {
out.println(
new String(messageFrame.getRevertReason().get().toArrayUnsafe(), UTF_8));
}
}
}
if (lastLoop && messageFrameStack.isEmpty()) {
final long evmGas = txGas - messageFrame.getRemainingGas();
final JsonObject resultLine = new JsonObject();
resultLine.put("gasUser", "0x" + Long.toHexString(evmGas));
if (!noTime) {
resultLine.put("timens", lastTime).put("time", lastTime / 1000);
}
resultLine
.put("gasTotal", "0x" + Long.toHexString(evmGas))
.put("output", messageFrame.getOutputData().toHexString());
out.println();
out.println(resultLine);
}
}
lastTime = stopwatch.elapsed().toNanos();
stopwatch.reset();
if (showJsonAlloc && lastLoop) {
updater.commit();
WorldState worldState = component.getWorldState();
dumpWorldState(worldState, out);
}
} while (remainingIters-- > 0);
} catch (final IOException e) {
System.err.println("Unable to create Genesis module");
e.printStackTrace(System.out);
}
}
public static void dumpWorldState(final WorldState worldState, final PrintWriter out) {
out.println("{");
worldState
.streamAccounts(Bytes32.ZERO, Integer.MAX_VALUE)
.sorted(Comparator.comparing(o -> o.getAddress().get().toHexString()))
.forEach(
account -> {
out.println(
" \"" + account.getAddress().map(Address::toHexString).orElse("-") + "\": {");
if (account.getCode() != null && !account.getCode().isEmpty()) {
out.println(" \"code\": \"" + account.getCode().toHexString() + "\",");
}
NavigableMap<Bytes32, AccountStorageEntry> storageEntries =
account.storageEntriesFrom(Bytes32.ZERO, Integer.MAX_VALUE);
if (!storageEntries.isEmpty()) {
out.println(" \"storage\": {");
out.println(
STORAGE_JOINER.join(
storageEntries.values().stream()
.map(
accountStorageEntry ->
" \""
+ accountStorageEntry
.getKey()
.map(UInt256::toQuantityHexString)
.orElse("-")
+ "\": \""
+ accountStorageEntry.getValue().toQuantityHexString()
+ "\"")
.toList()));
out.println(" },");
}
out.print(" \"balance\": \"" + account.getBalance().toShortHexString() + "\"");
if (account.getNonce() != 0) {
out.println(",");
out.println(
" \"nonce\": \""
+ Bytes.ofUnsignedLong(account.getNonce()).toShortHexString()
+ "\"");
} else {
out.println();
}
out.println(" },");
});
out.println("}");
out.flush();
}
}