/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.core;
import org.ethereum.config.BlockchainConfig;
import org.ethereum.config.CommonConfig;
import org.ethereum.config.SystemProperties;
import org.ethereum.db.BlockStore;
import org.ethereum.db.ContractDetails;
import org.ethereum.listener.EthereumListener;
import org.ethereum.listener.EthereumListenerAdapter;
import org.ethereum.util.ByteArraySet;
import org.ethereum.vm.*;
import org.ethereum.vm.program.Program;
import org.ethereum.vm.program.ProgramResult;
import org.ethereum.vm.program.invoke.ProgramInvoke;
import org.ethereum.vm.program.invoke.ProgramInvokeFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import java.math.BigInteger;
import java.util.List;
import static org.apache.commons.lang3.ArrayUtils.getLength;
import static org.apache.commons.lang3.ArrayUtils.isEmpty;
import static org.ethereum.util.BIUtil.*;
import static org.ethereum.util.ByteUtil.EMPTY_BYTE_ARRAY;
import static org.ethereum.util.ByteUtil.toHexString;
import static org.ethereum.vm.VMUtils.saveProgramTraceFile;
import static org.ethereum.vm.VMUtils.zipAndEncode;
/**
* @author Roman Mandeleil
* @since 19.12.2014
*/
public class TransactionExecutor {
private static final Logger logger = LoggerFactory.getLogger("execute");
private static final Logger stateLogger = LoggerFactory.getLogger("state");
SystemProperties config;
CommonConfig commonConfig;
BlockchainConfig blockchainConfig;
private Transaction tx;
private Repository track;
private Repository cacheTrack;
private BlockStore blockStore;
private final long gasUsedInTheBlock;
private boolean readyToExecute = false;
private String execError;
private ProgramInvokeFactory programInvokeFactory;
private byte[] coinbase;
private TransactionReceipt receipt;
private ProgramResult result = new ProgramResult();
private Block currentBlock;
private final EthereumListener listener;
private VM vm;
private Program program;
PrecompiledContracts.PrecompiledContract precompiledContract;
BigInteger m_endGas = BigInteger.ZERO;
long basicTxCost = 0;
List<LogInfo> logs = null;
private ByteArraySet touchedAccounts = new ByteArraySet();
boolean localCall = false;
public TransactionExecutor(Transaction tx, byte[] coinbase, Repository track, BlockStore blockStore,
ProgramInvokeFactory programInvokeFactory, Block currentBlock) {
this(tx, coinbase, track, blockStore, programInvokeFactory, currentBlock, new EthereumListenerAdapter(), 0);
}
public TransactionExecutor(Transaction tx, byte[] coinbase, Repository track, BlockStore blockStore,
ProgramInvokeFactory programInvokeFactory, Block currentBlock,
EthereumListener listener, long gasUsedInTheBlock) {
this.tx = tx;
this.coinbase = coinbase;
this.track = track;
this.cacheTrack = track.startTracking();
this.blockStore = blockStore;
this.programInvokeFactory = programInvokeFactory;
this.currentBlock = currentBlock;
this.listener = listener;
this.gasUsedInTheBlock = gasUsedInTheBlock;
this.m_endGas = toBI(tx.getGasLimit());
withCommonConfig(CommonConfig.getDefault());
}
public TransactionExecutor withCommonConfig(CommonConfig commonConfig) {
this.commonConfig = commonConfig;
this.config = commonConfig.systemProperties();
this.blockchainConfig = config.getBlockchainConfig().getConfigForBlock(currentBlock.getNumber());
return this;
}
private void execError(String err) {
logger.warn(err);
execError = err;
}
/**
* Do all the basic validation, if the executor
* will be ready to run the transaction at the end
* set readyToExecute = true
*/
public void init() {
basicTxCost = tx.transactionCost(config.getBlockchainConfig(), currentBlock);
if (localCall) {
readyToExecute = true;
return;
}
BigInteger txGasLimit = new BigInteger(1, tx.getGasLimit());
BigInteger curBlockGasLimit = new BigInteger(1, currentBlock.getGasLimit());
boolean cumulativeGasReached = txGasLimit.add(BigInteger.valueOf(gasUsedInTheBlock)).compareTo(curBlockGasLimit) > 0;
if (cumulativeGasReached) {
execError(String.format("Too much gas used in this block: Require: %s Got: %s", new BigInteger(1, currentBlock.getGasLimit()).longValue() - toBI(tx.getGasLimit()).longValue(), toBI(tx.getGasLimit()).longValue()));
return;
}
if (txGasLimit.compareTo(BigInteger.valueOf(basicTxCost)) < 0) {
execError(String.format("Not enough gas for transaction execution: Require: %s Got: %s", basicTxCost, txGasLimit));
return;
}
BigInteger reqNonce = track.getNonce(tx.getSender());
BigInteger txNonce = toBI(tx.getNonce());
if (isNotEqual(reqNonce, txNonce)) {
execError(String.format("Invalid nonce: required: %s , tx.nonce: %s", reqNonce, txNonce));
return;
}
BigInteger txGasCost = toBI(tx.getGasPrice()).multiply(txGasLimit);
BigInteger totalCost = toBI(tx.getValue()).add(txGasCost);
BigInteger senderBalance = track.getBalance(tx.getSender());
if (!isCovers(senderBalance, totalCost)) {
execError(String.format("Not enough cash: Require: %s, Sender cash: %s", totalCost, senderBalance));
return;
}
if (!blockchainConfig.acceptTransactionSignature(tx)) {
execError("Transaction signature not accepted: " + tx.getSignature());
return;
}
readyToExecute = true;
}
public void execute() {
if (!readyToExecute) return;
if (!localCall) {
track.increaseNonce(tx.getSender());
BigInteger txGasLimit = toBI(tx.getGasLimit());
BigInteger txGasCost = toBI(tx.getGasPrice()).multiply(txGasLimit);
track.addBalance(tx.getSender(), txGasCost.negate());
if (logger.isInfoEnabled())
logger.info("Paying: txGasCost: [{}], gasPrice: [{}], gasLimit: [{}]", txGasCost, toBI(tx.getGasPrice()), txGasLimit);
}
if (tx.isContractCreation()) {
create();
} else {
call();
}
}
private void call() {
if (!readyToExecute) return;
byte[] targetAddress = tx.getReceiveAddress();
precompiledContract = PrecompiledContracts.getContractForAddress(new DataWord(targetAddress));
if (precompiledContract != null) {
long requiredGas = precompiledContract.getGasForData(tx.getData());
if (!localCall && m_endGas.compareTo(BigInteger.valueOf(requiredGas + basicTxCost)) < 0) {
// no refund
// no endowment
execError("Out of Gas calling precompiled contract 0x" + Hex.toHexString(targetAddress) +
", required: " + (requiredGas + basicTxCost) + ", left: " + m_endGas);
m_endGas = BigInteger.ZERO;
return;
} else {
m_endGas = m_endGas.subtract(BigInteger.valueOf(requiredGas + basicTxCost));
// FIXME: save return for vm trace
byte[] out = precompiledContract.execute(tx.getData());
}
} else {
byte[] code = track.getCode(targetAddress);
if (isEmpty(code)) {
m_endGas = m_endGas.subtract(BigInteger.valueOf(basicTxCost));
result.spendGas(basicTxCost);
} else {
ProgramInvoke programInvoke =
programInvokeFactory.createProgramInvoke(tx, currentBlock, cacheTrack, blockStore);
this.vm = new VM(config);
this.program = new Program(track.getCodeHash(targetAddress), code, programInvoke, tx, config).withCommonConfig(commonConfig);
}
}
BigInteger endowment = toBI(tx.getValue());
transfer(cacheTrack, tx.getSender(), targetAddress, endowment);
touchedAccounts.add(targetAddress);
}
private void create() {
byte[] newContractAddress = tx.getContractAddress();
//In case of hashing collisions (for TCK tests only), check for any balance before createAccount()
BigInteger oldBalance = track.getBalance(newContractAddress);
cacheTrack.createAccount(tx.getContractAddress());
cacheTrack.addBalance(newContractAddress, oldBalance);
if (blockchainConfig.eip161()) {
cacheTrack.increaseNonce(newContractAddress);
}
if (isEmpty(tx.getData())) {
m_endGas = m_endGas.subtract(BigInteger.valueOf(basicTxCost));
result.spendGas(basicTxCost);
} else {
ProgramInvoke programInvoke = programInvokeFactory.createProgramInvoke(tx, currentBlock, cacheTrack, blockStore);
this.vm = new VM(config);
this.program = new Program(tx.getData(), programInvoke, tx, config).withCommonConfig(commonConfig);
// reset storage if the contract with the same address already exists
// TCK test case only - normally this is near-impossible situation in the real network
// TODO make via Trie.clear() without keyset
// ContractDetails contractDetails = program.getStorage().getContractDetails(newContractAddress);
// for (DataWord key : contractDetails.getStorageKeys()) {
// program.storageSave(key, DataWord.ZERO);
// }
}
BigInteger endowment = toBI(tx.getValue());
transfer(cacheTrack, tx.getSender(), newContractAddress, endowment);
touchedAccounts.add(newContractAddress);
}
public void go() {
if (!readyToExecute) return;
try {
if (vm != null) {
// Charge basic cost of the transaction
program.spendGas(tx.transactionCost(config.getBlockchainConfig(), currentBlock), "TRANSACTION COST");
if (config.playVM())
vm.play(program);
result = program.getResult();
m_endGas = toBI(tx.getGasLimit()).subtract(toBI(program.getResult().getGasUsed()));
if (tx.isContractCreation()) {
int returnDataGasValue = getLength(program.getResult().getHReturn()) *
blockchainConfig.getGasCost().getCREATE_DATA();
if (m_endGas.compareTo(BigInteger.valueOf(returnDataGasValue)) < 0) {
// Not enough gas to return contract code
if (!blockchainConfig.getConstants().createEmptyContractOnOOG()) {
program.setRuntimeFailure(Program.Exception.notEnoughSpendingGas("No gas to return just created contract",
returnDataGasValue, program));
result = program.getResult();
}
result.setHReturn(EMPTY_BYTE_ARRAY);
} else if (getLength(result.getHReturn()) > blockchainConfig.getConstants().getMAX_CONTRACT_SZIE()) {
// Contract size too large
program.setRuntimeFailure(Program.Exception.notEnoughSpendingGas("Contract size too large: " + getLength(result.getHReturn()),
returnDataGasValue, program));
result = program.getResult();
result.setHReturn(EMPTY_BYTE_ARRAY);
} else {
// Contract successfully created
m_endGas = m_endGas.subtract(BigInteger.valueOf(returnDataGasValue));
cacheTrack.saveCode(tx.getContractAddress(), result.getHReturn());
}
}
String err = config.getBlockchainConfig().getConfigForBlock(currentBlock.getNumber()).
validateTransactionChanges(blockStore, currentBlock, tx, null);
if (err != null) {
program.setRuntimeFailure(new RuntimeException("Transaction changes validation failed: " + err));
}
if (result.getException() != null) {
result.getDeleteAccounts().clear();
result.getLogInfoList().clear();
result.resetFutureRefund();
throw result.getException();
}
touchedAccounts.addAll(result.getTouchedAccounts());
}
cacheTrack.commit();
} catch (Throwable e) {
// TODO: catch whatever they will throw on you !!!
// https://github.com/ethereum/cpp-ethereum/blob/develop/libethereum/Executive.cpp#L241
cacheTrack.rollback();
m_endGas = BigInteger.ZERO;
execError(e.getMessage());
}
}
public TransactionExecutionSummary finalization() {
if (!readyToExecute) return null;
TransactionExecutionSummary.Builder summaryBuilder = TransactionExecutionSummary.builderFor(tx)
.gasLeftover(m_endGas)
.logs(result.getLogInfoList())
.result(result.getHReturn());
if (result != null) {
// Accumulate refunds for suicides
result.addFutureRefund(result.getDeleteAccounts().size() * config.getBlockchainConfig().
getConfigForBlock(currentBlock.getNumber()).getGasCost().getSUICIDE_REFUND());
long gasRefund = Math.min(result.getFutureRefund(), getGasUsed() / 2);
byte[] addr = tx.isContractCreation() ? tx.getContractAddress() : tx.getReceiveAddress();
m_endGas = m_endGas.add(BigInteger.valueOf(gasRefund));
summaryBuilder
.gasUsed(toBI(result.getGasUsed()))
.gasRefund(toBI(gasRefund))
.deletedAccounts(result.getDeleteAccounts())
.internalTransactions(result.getInternalTransactions());
ContractDetails contractDetails = track.getContractDetails(addr);
if (contractDetails != null) {
// TODO
// summaryBuilder.storageDiff(track.getContractDetails(addr).getStorage());
//
// if (program != null) {
// summaryBuilder.touchedStorage(contractDetails.getStorage(), program.getStorageDiff());
// }
}
if (result.getException() != null) {
summaryBuilder.markAsFailed();
}
}
TransactionExecutionSummary summary = summaryBuilder.build();
// Refund for gas leftover
track.addBalance(tx.getSender(), summary.getLeftover().add(summary.getRefund()));
logger.info("Pay total refund to sender: [{}], refund val: [{}]", Hex.toHexString(tx.getSender()), summary.getRefund());
// Transfer fees to miner
track.addBalance(coinbase, summary.getFee());
touchedAccounts.add(coinbase);
logger.info("Pay fees to miner: [{}], feesEarned: [{}]", Hex.toHexString(coinbase), summary.getFee());
if (result != null) {
logs = result.getLogInfoList();
// Traverse list of suicides
for (DataWord address : result.getDeleteAccounts()) {
track.delete(address.getLast20Bytes());
}
}
if (blockchainConfig.eip161()) {
for (byte[] acctAddr : touchedAccounts) {
AccountState state = track.getAccountState(acctAddr);
if (state != null && state.isEmpty()) {
track.delete(acctAddr);
}
}
}
listener.onTransactionExecuted(summary);
if (config.vmTrace() && program != null && result != null) {
String trace = program.getTrace()
.result(result.getHReturn())
.error(result.getException())
.toString();
if (config.vmTraceCompressed()) {
trace = zipAndEncode(trace);
}
String txHash = toHexString(tx.getHash());
saveProgramTraceFile(config, txHash, trace);
listener.onVMTraceCreated(txHash, trace);
}
return summary;
}
public TransactionExecutor setLocalCall(boolean localCall) {
this.localCall = localCall;
return this;
}
public TransactionReceipt getReceipt() {
if (receipt == null) {
receipt = new TransactionReceipt();
long totalGasUsed = gasUsedInTheBlock + getGasUsed();
receipt.setCumulativeGas(totalGasUsed);
receipt.setTransaction(tx);
receipt.setLogInfoList(getVMLogs());
receipt.setGasUsed(getGasUsed());
receipt.setExecutionResult(getResult().getHReturn());
receipt.setError(execError);
// receipt.setPostTxState(track.getRoot()); // TODO later when RepositoryTrack.getRoot() is implemented
}
return receipt;
}
public List<LogInfo> getVMLogs() {
return logs;
}
public ProgramResult getResult() {
return result;
}
public long getGasUsed() {
return toBI(tx.getGasLimit()).subtract(m_endGas).longValue();
}
}